From 52430f914ceea555176fd13ff25e4dd61d1a6dc9 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:37:12 +0000 Subject: [PATCH 1/7] fix(updater): Persist key-scoped fetched edition hints Store the last known-good fetched core info edition in node.updater and scope it to the active update key so stale hints are ignored after key changes. Seed core updater subscription from max(currentBuild, lastKnownGoodFetchedEdition) and reset hints when the configured update key changes. Also keep key-only URI compatibility tests and add focused tests for seed selection, key mismatch reset, and fetch recording behavior. --- .../node/updater/NodeUpdateManager.java | 333 +++++++++++++++++- .../crypta/node/updater/NodeUpdater.java | 18 +- .../node/updater/NodeUpdaterParams.java | 4 +- .../crypta/l10n/crypta.l10n.en.properties | 6 +- .../node/updater/NodeUpdateManagerTest.java | 188 +++++++++- 5 files changed, 524 insertions(+), 25 deletions(-) diff --git a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java index 61f65aac7de..4ee2cc7b8fa 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java @@ -35,6 +35,7 @@ import network.crypta.support.HTMLNode; import network.crypta.support.api.BooleanCallback; import network.crypta.support.api.Bucket; +import network.crypta.support.api.IntCallback; import network.crypta.support.api.StringCallback; import network.crypta.support.io.BucketTools; import network.crypta.support.io.FileUtil; @@ -83,6 +84,17 @@ public final class NodeUpdateManager { // L10n parameter keys and repeated URL query parts private static final String L10N_PARAM_ERROR = "error"; private static final String QUERY_TEXT_PLAIN = "?type=text/plain"; + private static final String URI_TYPE_SEPARATOR = "@"; + private static final String URI_PATH_SEPARATOR = "/"; + private static final String UPDATE_URI_PREFIX = "USK@"; + private static final String UPDATE_URI_DOC_NAME = "info"; + private static final String LEGACY_UPDATE_URI_DOC_NAME = "jar"; + private static final String REVOCATION_URI_PREFIX = "SSK@"; + private static final String REVOCATION_URI_DOC_NAME = "revoked"; + private static final String LAST_KNOWN_GOOD_FETCHED_EDITION_OPTION = + "lastKnownGoodFetchedEdition"; + private static final String LAST_KNOWN_GOOD_FETCHED_EDITION_KEY_OPTION = + "lastKnownGoodFetchedEditionKey"; /** * The last build on the previous key with Java 7 support. Older nodes can update to this point @@ -90,14 +102,13 @@ public final class NodeUpdateManager { */ public static final int TRANSITION_VERSION = 1481; - /** The URI for post-TRANSITION_VERSION builds' freenet.jar. */ + /** Public key material for post-TRANSITION_VERSION update URIs. */ public static final String UPDATE_URI = - "USK@uQnFwn0aEFSAZihnSDduEHUd3GUmGg68ATn5R95MKJo,mcNiZqosfZ1F~PkZY8v1TuDKsY6noda-hGRXvu7uUFc,AQACAAE/jar/" - + Version.currentBuildNumber(); + "uQnFwn0aEFSAZihnSDduEHUd3GUmGg68ATn5R95MKJo,mcNiZqosfZ1F~PkZY8v1TuDKsY6noda-hGRXvu7uUFc,AQACAAE"; - /** Default USK/SSK pointing to revocation content that disables auto‑update. */ + /** Public key material used to derive the revocation key URI. */ public static final String REVOCATION_URI = - "SSK@TAnVLWtrGguuIi3fXkf8OmT5Pmy2Hduai18FUCP0uAU,tMg8t4kLktzmz~uFC6jk~-CUNv1mQ-C573sjLeg0alU,AQACAAE/revoked"; + "TAnVLWtrGguuIi3fXkf8OmT5Pmy2Hduai18FUCP0uAU,tMg8t4kLktzmz~uFC6jk~-CUNv1mQ-C573sjLeg0alU,AQACAAE"; // These are necessary to prevent DoS. /** Maximum allowed decoded byte length of a revocation document. */ @@ -134,6 +145,8 @@ public final class NodeUpdateManager { private FreenetURI updateURI; private FreenetURI revocationURI; + private volatile int lastKnownGoodFetchedEdition; + private volatile String lastKnownGoodFetchedEditionKey; // Legacy MainJarUpdater removed; core package updater is used instead. // Package-based core updater (Kotlin) @@ -230,14 +243,15 @@ public NodeUpdateManager(Node node, Config config) throws InvalidConfigValueExce 3, true, true, "NodeUpdateManager.updateURI", "NodeUpdateManager.updateURILong"), new UpdateURICallback()); + String configuredUpdateUriValue = updaterConfig.getString("URI"); try { - updateURI = new FreenetURI(updaterConfig.getString("URI")); + updateURI = parseConfiguredUpdateURI(configuredUpdateUriValue); } catch (MalformedURLException e) { throw new InvalidConfigValueException( l10n("invalidUpdateURI", L10N_PARAM_ERROR, e.getLocalizedMessage())); } + migrateLegacyUpdateUriValueIfNeeded(updaterConfig, configuredUpdateUriValue); - updateURI = updateURI.setSuggestedEdition(Version.currentBuildNumber()); if (updateURI.hasMetaStrings()) { throw new InvalidConfigValueException(l10n("updateURIMustHaveNoMetaStrings")); } @@ -256,12 +270,43 @@ public NodeUpdateManager(Node node, Config config) throws InvalidConfigValueExce "NodeUpdateManager.revocationURILong"), new UpdateRevocationURICallback()); + String configuredRevocationUriValue = updaterConfig.getString("revocationURI"); try { - revocationURI = new FreenetURI(updaterConfig.getString("revocationURI")); + revocationURI = parseConfiguredRevocationURI(configuredRevocationUriValue); } catch (MalformedURLException e) { throw new InvalidConfigValueException( l10n("invalidRevocationURI", L10N_PARAM_ERROR, e.getLocalizedMessage())); } + migrateLegacyRevocationUriValueIfNeeded(updaterConfig, configuredRevocationUriValue); + + updaterConfig.register( + LAST_KNOWN_GOOD_FETCHED_EDITION_OPTION, + -1, + new Option.Meta( + 5, + true, + false, + "NodeUpdateManager.lastKnownGoodFetchedEdition", + "NodeUpdateManager.lastKnownGoodFetchedEditionLong"), + new LastKnownGoodFetchedEditionCallback(), + false); + lastKnownGoodFetchedEdition = + sanitizeFetchedEdition(updaterConfig.getInt(LAST_KNOWN_GOOD_FETCHED_EDITION_OPTION)); + + updaterConfig.register( + LAST_KNOWN_GOOD_FETCHED_EDITION_KEY_OPTION, + UPDATE_URI, + new Option.Meta( + 6, + true, + false, + "NodeUpdateManager.lastKnownGoodFetchedEditionKey", + "NodeUpdateManager.lastKnownGoodFetchedEditionKeyLong"), + new LastKnownGoodFetchedEditionKeyCallback()); + lastKnownGoodFetchedEditionKey = + sanitizePublicKeyMaterial( + updaterConfig.getString(LAST_KNOWN_GOOD_FETCHED_EDITION_KEY_OPTION)); + alignLastKnownGoodFetchedEditionToCurrentUpdateKey(); // Deprecated UI option: updateSeednodes (no longer shown on the Auto-update page). // Keep internal default as false; accept but ignore legacy config values. @@ -747,18 +792,63 @@ public synchronized void addChangelogLinks(long version, HTMLNode node) { */ public synchronized void setURI(FreenetURI uri) { NodeUpdater updater; + int subscribeEditionSeed; synchronized (this) { if (updateURI.equals(uri)) { return; } + String oldPublicKey = extractPublicKeyMaterial(updateURI); updateURI = uri; updateURI = updateURI.setSuggestedEdition(Version.currentBuildNumber()); + String newPublicKey = extractPublicKeyMaterial(updateURI); + if (!newPublicKey.equals(oldPublicKey)) { + resetLastKnownGoodFetchedEditionLocked(newPublicKey); + } + subscribeEditionSeed = computeCoreUpdaterSubscribeEditionSeedLocked(newPublicKey); updater = coreUpdater; if (updater == null) { return; } } - updater.onChangeURI(uri); + updater.onChangeURI(uri, subscribeEditionSeed); + } + + /** + * Records a successfully fetched core-info edition for startup seeding. + * + *
The hint is key-scoped: editions fetched from a stale or different key are ignored. + */ + void recordSuccessfulCoreInfoFetch(FreenetURI fetchedUri, int fetchedEdition) { + if (fetchedEdition < 0 || fetchedUri == null) { + return; + } + synchronized (this) { + String fetchedPublicKey = extractPublicKeyMaterial(fetchedUri); + String currentPublicKey = extractPublicKeyMaterial(updateURI); + if (!currentPublicKey.equals(fetchedPublicKey)) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Ignoring fetched edition {} for stale key {}; current key {}", + fetchedEdition, + fetchedPublicKey, + currentPublicKey); + } + return; + } + if (!currentPublicKey.equals(lastKnownGoodFetchedEditionKey)) { + lastKnownGoodFetchedEditionKey = currentPublicKey; + lastKnownGoodFetchedEdition = -1; + } + if (fetchedEdition > lastKnownGoodFetchedEdition) { + lastKnownGoodFetchedEdition = fetchedEdition; + if (LOG.isDebugEnabled()) { + LOG.debug( + "Recorded last known good fetched edition {} for key {}", + lastKnownGoodFetchedEdition, + currentPublicKey); + } + } + } } /** @@ -1081,18 +1171,44 @@ public void set(Boolean val) throws InvalidConfigValueException { } } + class LastKnownGoodFetchedEditionCallback extends IntCallback { + + @Override + public Integer get() { + return lastKnownGoodFetchedEdition; + } + + @Override + public void set(Integer val) { + lastKnownGoodFetchedEdition = sanitizeFetchedEdition(val); + } + } + + class LastKnownGoodFetchedEditionKeyCallback extends StringCallback { + + @Override + public String get() { + return lastKnownGoodFetchedEditionKey; + } + + @Override + public void set(String val) { + lastKnownGoodFetchedEditionKey = sanitizePublicKeyMaterial(val); + } + } + class UpdateURICallback extends StringCallback { @Override public String get() { - return getURI().toString(false, false); + return extractPublicKeyMaterial(getURI()); } @Override public void set(String val) throws InvalidConfigValueException { FreenetURI uri; try { - uri = new FreenetURI(val); + uri = parseConfiguredUpdateURI(val); } catch (MalformedURLException e) { throw new InvalidConfigValueException( l10n("invalidUpdateURI", L10N_PARAM_ERROR, e.getLocalizedMessage())); @@ -1116,14 +1232,14 @@ public UpdateRevocationURICallback() { @Override public String get() { - return getRevocationURI().toString(false, false); + return extractPublicKeyMaterial(getRevocationURI()); } @Override public void set(String val) throws InvalidConfigValueException { FreenetURI uri; try { - uri = new FreenetURI(val); + uri = parseConfiguredRevocationURI(val); } catch (MalformedURLException e) { throw new InvalidConfigValueException( l10n("invalidRevocationURI", L10N_PARAM_ERROR, e.getLocalizedMessage())); @@ -1132,6 +1248,192 @@ public void set(String val) throws InvalidConfigValueException { } } + private static FreenetURI parseConfiguredUpdateURI(String configuredValue) + throws MalformedURLException { + String normalizedLegacyKey = extractLegacyUpdatePublicKeyMaterial(configuredValue); + if (normalizedLegacyKey != null) { + FreenetURI parsed = new FreenetURI(expandUpdateUriFromPublicKey(normalizedLegacyKey)); + return parsed.setSuggestedEdition(Version.currentBuildNumber()); + } + + FreenetURI parsed = new FreenetURI(trimConfigValue(configuredValue)); + return parsed.setSuggestedEdition(Version.currentBuildNumber()); + } + + private static FreenetURI parseConfiguredRevocationURI(String configuredValue) + throws MalformedURLException { + String normalizedLegacyKey = extractLegacyRevocationPublicKeyMaterial(configuredValue); + if (normalizedLegacyKey != null) { + return new FreenetURI(expandRevocationUriFromPublicKey(normalizedLegacyKey)); + } + return new FreenetURI(trimConfigValue(configuredValue)); + } + + private static String extractLegacyUpdatePublicKeyMaterial(String configuredValue) + throws MalformedURLException { + String trimmed = trimConfigValue(configuredValue); + if (isBarePublicKey(trimmed)) { + return trimmed; + } + + FreenetURI parsed = new FreenetURI(trimmed); + if (!parsed.isUSK() || parsed.hasMetaStrings()) { + return null; + } + String docName = parsed.getDocName(); + if (!UPDATE_URI_DOC_NAME.equals(docName) && !LEGACY_UPDATE_URI_DOC_NAME.equals(docName)) { + return null; + } + return extractPublicKeyMaterial(parsed); + } + + private static String extractLegacyRevocationPublicKeyMaterial(String configuredValue) + throws MalformedURLException { + String trimmed = trimConfigValue(configuredValue); + if (isBarePublicKey(trimmed)) { + return trimmed; + } + + FreenetURI parsed = new FreenetURI(trimmed); + if (!parsed.isSSK() || parsed.hasMetaStrings()) { + return null; + } + if (!REVOCATION_URI_DOC_NAME.equals(parsed.getDocName())) { + return null; + } + return extractPublicKeyMaterial(parsed); + } + + private static String expandUpdateUriFromPublicKey(String keyMaterial) { + return UPDATE_URI_PREFIX + + keyMaterial + + URI_PATH_SEPARATOR + + UPDATE_URI_DOC_NAME + + URI_PATH_SEPARATOR + + Version.currentBuildNumber(); + } + + private static String expandRevocationUriFromPublicKey(String keyMaterial) { + return REVOCATION_URI_PREFIX + keyMaterial + URI_PATH_SEPARATOR + REVOCATION_URI_DOC_NAME; + } + + private void migrateLegacyUpdateUriValueIfNeeded(SubConfig updaterConfig, String configuredValue) + throws InvalidConfigValueException { + migrateLegacyOptionValueIfNeeded( + updaterConfig, + "URI", + configuredValue, + NodeUpdateManager::extractLegacyUpdatePublicKeyMaterial); + } + + private void migrateLegacyRevocationUriValueIfNeeded( + SubConfig updaterConfig, String configuredValue) throws InvalidConfigValueException { + migrateLegacyOptionValueIfNeeded( + updaterConfig, + "revocationURI", + configuredValue, + NodeUpdateManager::extractLegacyRevocationPublicKeyMaterial); + } + + private static void migrateLegacyOptionValueIfNeeded( + SubConfig updaterConfig, + String optionName, + String configuredValue, + LegacyKeyExtractor extractor) + throws InvalidConfigValueException { + if (isBarePublicKey(trimConfigValue(configuredValue))) { + return; + } + + String extracted; + try { + extracted = extractor.extract(configuredValue); + } catch (MalformedURLException e) { + throw new InvalidConfigValueException(e.getLocalizedMessage()); + } + if (extracted == null) { + return; + } + + Option> option = updaterConfig.getOption(optionName); + if (option == null) { + return; + } + option.setInitialValue(extracted); + } + + private static String extractPublicKeyMaterial(FreenetURI uri) { + String fullUri = uri.toString(false, false); + int typeSeparator = fullUri.indexOf(URI_TYPE_SEPARATOR); + if (typeSeparator < 0) { + return fullUri; + } + int pathSeparator = fullUri.indexOf(URI_PATH_SEPARATOR, typeSeparator + 1); + if (pathSeparator < 0) { + return fullUri.substring(typeSeparator + 1); + } + return fullUri.substring(typeSeparator + 1, pathSeparator); + } + + private static int sanitizeFetchedEdition(Integer edition) { + if (edition == null) { + return -1; + } + return Math.max(-1, edition); + } + + private static String sanitizePublicKeyMaterial(String value) { + String trimmed = trimConfigValue(value); + if (trimmed == null || trimmed.isEmpty()) { + return ""; + } + return isBarePublicKey(trimmed) ? trimmed : ""; + } + + private synchronized void alignLastKnownGoodFetchedEditionToCurrentUpdateKey() { + String currentUpdatePublicKey = extractPublicKeyMaterial(updateURI); + if (!currentUpdatePublicKey.equals(lastKnownGoodFetchedEditionKey)) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Resetting persisted fetched edition {} due to key mismatch: persisted={}, current={}", + lastKnownGoodFetchedEdition, + lastKnownGoodFetchedEditionKey, + currentUpdatePublicKey); + } + resetLastKnownGoodFetchedEditionLocked(currentUpdatePublicKey); + return; + } + lastKnownGoodFetchedEdition = sanitizeFetchedEdition(lastKnownGoodFetchedEdition); + } + + private int computeCoreUpdaterSubscribeEditionSeedLocked(String currentUpdatePublicKey) { + if (!currentUpdatePublicKey.equals(lastKnownGoodFetchedEditionKey)) { + return Version.currentBuildNumber(); + } + return Math.max(Version.currentBuildNumber(), lastKnownGoodFetchedEdition); + } + + private void resetLastKnownGoodFetchedEditionLocked(String currentUpdatePublicKey) { + lastKnownGoodFetchedEdition = -1; + lastKnownGoodFetchedEditionKey = currentUpdatePublicKey; + } + + private static boolean isBarePublicKey(String value) { + if (value == null || value.isEmpty()) { + return false; + } + return !value.contains(URI_TYPE_SEPARATOR) && !value.contains(URI_PATH_SEPARATOR); + } + + private static String trimConfigValue(String value) { + return value == null ? null : value.trim(); + } + + @FunctionalInterface + private interface LegacyKeyExtractor { + String extract(String configuredValue) throws MalformedURLException; + } + /** * Called when a peer indicates in its UOMAnnounce that it has fetched the revocation key (or * failed to do so in a way suggesting that somebody knows the key). @@ -1286,6 +1588,8 @@ public ByteCounter getByteCounter() { /** Create and wire the package‑based {@link CoreUpdater} if not already present. */ public synchronized void startCoreUpdater() { if (coreUpdater != null) return; + int subscribeEditionSeed = + computeCoreUpdaterSubscribeEditionSeedLocked(extractPublicKeyMaterial(updateURI)); NodeUpdaterParams params = new NodeUpdaterParams( this, @@ -1293,7 +1597,8 @@ public synchronized void startCoreUpdater() { Version.currentBuildNumber(), -1, Integer.MAX_VALUE, - "core-info-"); + "core-info-", + subscribeEditionSeed); coreUpdater = new CoreUpdater(params); } diff --git a/src/main/java/network/crypta/node/updater/NodeUpdater.java b/src/main/java/network/crypta/node/updater/NodeUpdater.java index 7c4d391b8be..99155f71c21 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdater.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdater.java @@ -28,7 +28,6 @@ import network.crypta.node.NodeClientCore; import network.crypta.node.RequestClient; import network.crypta.node.RequestStarter; -import network.crypta.node.Version; import network.crypta.support.Ticker; import network.crypta.support.api.Bucket; import network.crypta.support.api.RandomAccessBucket; @@ -139,7 +138,7 @@ public abstract class NodeUpdater implements ClientGetCallback, USKCallback, Req // Debug gating derives from LOG.isDebugEnabled() where needed this.manager = params.manager(); this.node = manager.getNode(); - this.uri = params.updateUri().setSuggestedEdition(((long) Version.currentBuildNumber()) + 1); + this.uri = params.updateUri().setSuggestedEdition(params.subscribeEditionSeed()); this.ticker = node.network().ticker(); this.core = node.services().clientCore(); this.currentVersion = params.current(); @@ -163,12 +162,11 @@ void start() { private void subscribe(Runnable onError) { try { - // because of UoM, this version is actually worth having as well FreenetURI localUri; synchronized (this) { localUri = this.uri; } - USK myUsk = USK.create(localUri.setSuggestedEdition(currentVersion)); + USK myUsk = USK.create(localUri); core.getUskManager().subscribe(myUsk, this, true, getRequestClient()); } catch (MalformedURLException _) { LOG.error("The auto-update URI isn't valid and can't be used"); @@ -357,14 +355,15 @@ RandomAccessBucket getBlobBucket(int availableVersion) { public void onSuccess(FetchResult result, ClientGetter state) { File localTempBlobFile; int localFetchingVersion; + FreenetURI fetchedUri = state != null ? state.getURI() : null; synchronized (this) { localTempBlobFile = tempBlobFile; localFetchingVersion = fetchingVersion; } - onSuccess(result, localTempBlobFile, localFetchingVersion); + onSuccess(result, localTempBlobFile, localFetchingVersion, fetchedUri); } - void onSuccess(FetchResult result, File tempBlobFile, int fetchedVersion) { + void onSuccess(FetchResult result, File tempBlobFile, int fetchedVersion, FreenetURI fetchedUri) { // Debug gating derives from LOG.isDebugEnabled() where needed File blobFile; synchronized (this) { @@ -393,6 +392,8 @@ void onSuccess(FetchResult result, File tempBlobFile, int fetchedVersion) { this.cg = null; } processSuccess(fetchedVersion, result, blobFile); + manager.recordSuccessfulCoreInfoFetch( + fetchedUri != null ? fetchedUri : getUpdateKey(), fetchedVersion); } private boolean shouldSkipAlreadyFetched(int fetchedVersion) { @@ -755,8 +756,9 @@ public synchronized boolean canUpdateNow() { * Called when the fetch URI has changed. The caller holds no major locks. * * @param newUri the new update key; its doc name is preserved when the argument omits one + * @param subscribeEditionSeed edition to use when subscribing to the new update key */ - public void onChangeURI(FreenetURI newUri) { + public void onChangeURI(FreenetURI newUri, int subscribeEditionSeed) { String previousDocName; synchronized (this) { previousDocName = (this.uri != null) ? this.uri.getDocName() : null; @@ -767,7 +769,7 @@ public void onChangeURI(FreenetURI newUri) { ? newUri.setDocName(previousDocName) : newUri; synchronized (this) { - this.uri = nextUri.setSuggestedEdition(((long) Version.currentBuildNumber()) + 1); + this.uri = nextUri.setSuggestedEdition(subscribeEditionSeed); availableVersion = -1; realAvailableVersion = -1; fetchingVersion = -1; diff --git a/src/main/java/network/crypta/node/updater/NodeUpdaterParams.java b/src/main/java/network/crypta/node/updater/NodeUpdaterParams.java index 12cb3986454..a47e719e96d 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdaterParams.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdaterParams.java @@ -28,6 +28,7 @@ * @param min minimum acceptable deployment build number for this node. * @param max maximum acceptable deployment build number for this node. * @param blobFilenamePrefix prefix for update blob filenames stored on disk. + * @param subscribeEditionSeed initial USK edition used when subscribing for update discovery. * @see NodeUpdateManager * @see NodeUpdater */ @@ -37,4 +38,5 @@ public record NodeUpdaterParams( int current, int min, int max, - String blobFilenamePrefix) {} + String blobFilenamePrefix, + int subscribeEditionSeed) {} diff --git a/src/main/resources/network/crypta/l10n/crypta.l10n.en.properties b/src/main/resources/network/crypta/l10n/crypta.l10n.en.properties index 35851904892..1ade165676c 100644 --- a/src/main/resources/network/crypta/l10n/crypta.l10n.en.properties +++ b/src/main/resources/network/crypta/l10n/crypta.l10n.en.properties @@ -1445,6 +1445,10 @@ NodeUpdateManager.enabled=Check for and download new versions NodeUpdateManager.enabledLong=Should your node automatically check for new versions of Crypta? If yes, new versions will be automatically detected and downloaded, but not necessarily installed. This setting resets itself always back to false unless your Crypta node runs within the wrapper. NodeUpdateManager.installNewVersions=Automatically download updates NodeUpdateManager.installNewVersionsLong=Should your node automatically download new Crypta releases in the background and show an Install button when ready? Installation is manual via your operating system. +NodeUpdateManager.lastKnownGoodFetchedEdition=Last known-good fetched update edition +NodeUpdateManager.lastKnownGoodFetchedEditionLong=Internal updater hint storing the most recent successfully fetched core info edition for the current update key. +NodeUpdateManager.lastKnownGoodFetchedEditionKey=Update key for last known-good fetched edition +NodeUpdateManager.lastKnownGoodFetchedEditionKeyLong=Internal updater hint storing the update public key associated with the last known-good fetched edition. NodeUpdateManager.invalidRevocationURI=Invalid revocation URI: ${error} NodeUpdateManager.invalidUpdateURI=Invalid update URI: ${error} NodeUpdateManager.noUpdateWithoutWrapper=Cannot update because not running under the Java Service Wrapper @@ -1459,7 +1463,7 @@ NodeUpdateManager.updateSeednodesLong=Update the seednodes.fref file automatical NodeUpdateManager.updateInstallers=Download the latest installers over Crypta? NodeUpdateManager.updateInstallersLong=Download the latest installers over Crypta? These will be linked from the "Add a Friend" page. NodeUpdateManager.updateURI=Where should Crypta look for updates? -NodeUpdateManager.updateURILong=Where should your Crypta node look for updates? This should be a Crypta URI where updates can be downloaded from. The default URI ends in /jar/(version number). To download the source code for an update, you can copy the key below, and change "jar" to "update-source." Similarly, "changelog" gives you the user-facing list of changes, "fullchangelog" gives you the long, developer-friendly list of changes, and "installer," "wininstaller," "seednodes" and "iptocountryv4" give other files. +NodeUpdateManager.updateURILong=Where should your Crypta node look for updates? This should be a Crypta URI where updates can be downloaded from. The default URI ends in /info/(version number). You can copy the key below and change "info" to "changelog" for the user-facing list of changes, "fullchangelog" for the long developer-facing list, or "installer", "wininstaller", "seednodes", and "iptocountryv4" for other update-related files. NodeUpdateManager.updateURIMustBeAUSK=Update URI must be a USK! NodeUpdateManager.updateURIMustHaveNoMetaStrings=Update URI must not have extra path components, e.g. USK@blah,blah,blah/update/1430 not USK@blah,blah,blah/update/1430/ NodeUpdateManager.updateFailedInternalError=FAILED TO UPDATE Crypta. INTERNAL ERROR: ${reason} diff --git a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java index 4c0ad03630a..b9bca50bd60 100644 --- a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java +++ b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java @@ -10,6 +10,7 @@ import network.crypta.client.async.ClientContext; import network.crypta.client.events.SimpleEventProducer; import network.crypta.config.Config; +import network.crypta.config.PersistentConfig; import network.crypta.io.comm.Message; import network.crypta.keys.FreenetURI; import network.crypta.node.Node; @@ -24,6 +25,7 @@ import network.crypta.node.useralerts.UserAlert; import network.crypta.node.useralerts.UserAlertManager; import network.crypta.support.HTMLNode; +import network.crypta.support.SimpleFieldSet; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,6 +67,7 @@ class NodeUpdateManagerTest { @Mock NodeStats nodeStats; private NodeUpdateManager manager; + private Config config; @BeforeEach void setUp() throws Exception { @@ -101,7 +104,8 @@ void setUp() throws Exception { ClientContext clientContext = Mockito.mock(ClientContext.class); when(nodeCore.getClientContext()).thenReturn(clientContext); - manager = new NodeUpdateManager(node, new Config()); + config = new Config(); + manager = new NodeUpdateManager(node, config); } private static @NotNull FetchContext getFetchContext() { @@ -175,6 +179,188 @@ void setURI_whenDifferentAndCoreUpdaterPresent_expectNoException_andUriUpdated() manager.getURI().toString(false, false)); } + @Test + void updateUriCallback_whenGivenPublicKeyOnly_expectExpandedToUskInfoUri() throws Exception { + // Arrange + NodeUpdateManager.UpdateURICallback callback = manager.new UpdateURICallback(); + + // Act + callback.set(NodeUpdateManager.UPDATE_URI); + + // Assert + String expected = + "USK@" + NodeUpdateManager.UPDATE_URI + "/info/" + Version.currentBuildNumber(); + assertEquals(expected, manager.getURI().toString(false, false)); + } + + @Test + void revocationUriCallback_whenGivenPublicKeyOnly_expectExpandedToSskRevokedUri() + throws Exception { + // Arrange + NodeUpdateManager.UpdateRevocationURICallback callback = + manager.new UpdateRevocationURICallback(); + + // Act + callback.set(NodeUpdateManager.REVOCATION_URI); + + // Assert + String expected = "SSK@" + NodeUpdateManager.REVOCATION_URI + "/revoked"; + assertEquals(expected, manager.getRevocationURI().toString(false, false)); + } + + @Test + void uriCallbacks_get_expectPublicKeyOnly() { + // Arrange + NodeUpdateManager.UpdateURICallback updateCallback = manager.new UpdateURICallback(); + NodeUpdateManager.UpdateRevocationURICallback revocationCallback = + manager.new UpdateRevocationURICallback(); + + // Act + Assert + assertEquals(NodeUpdateManager.UPDATE_URI, updateCallback.get()); + assertEquals(NodeUpdateManager.REVOCATION_URI, revocationCallback.get()); + } + + @Test + void constructor_whenLegacyFullUrisPersisted_expectAcceptedAndCanonicalizedToBareKeys() + throws Exception { + // Arrange + SimpleFieldSet persisted = new SimpleFieldSet(true); + persisted.putSingle("node.updater.URI", "USK@" + NodeUpdateManager.UPDATE_URI + "/jar/1481"); + persisted.putSingle( + "node.updater.revocationURI", "SSK@" + NodeUpdateManager.REVOCATION_URI + "/revoked"); + PersistentConfig config = new PersistentConfig(persisted); + + // Act + NodeUpdateManager migrated = new NodeUpdateManager(node, config); + + // Assert: updater URI uses current build and default info docname + String expectedUpdate = + "USK@" + NodeUpdateManager.UPDATE_URI + "/info/" + Version.currentBuildNumber(); + assertEquals(expectedUpdate, migrated.getURI().toString(false, false)); + assertEquals( + "SSK@" + NodeUpdateManager.REVOCATION_URI + "/revoked", + migrated.getRevocationURI().toString(false, false)); + + // Assert: persisted option values are canonical bare keys after migration + assertEquals(NodeUpdateManager.UPDATE_URI, config.get("node.updater").getString("URI")); + assertEquals( + NodeUpdateManager.REVOCATION_URI, config.get("node.updater").getString("revocationURI")); + } + + @Test + void + startCoreUpdater_whenMatchingPersistedEditionHigherThanCurrent_expectSubscribeSeedFromEdition() + throws Exception { + // Arrange + int seededEdition = Version.currentBuildNumber() + 7; + SimpleFieldSet persisted = new SimpleFieldSet(true); + persisted.put("node.updater.lastKnownGoodFetchedEdition", seededEdition); + persisted.putSingle( + "node.updater.lastKnownGoodFetchedEditionKey", NodeUpdateManager.UPDATE_URI); + PersistentConfig persistedConfig = new PersistentConfig(persisted); + NodeUpdateManager seeded = new NodeUpdateManager(node, persistedConfig); + + // Act + seeded.startCoreUpdater(); + + // Assert + assertEquals(seededEdition, seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition()); + } + + @Test + void startCoreUpdater_whenMatchingPersistedEditionLowerThanCurrent_expectSubscribeSeedAtCurrent() + throws Exception { + // Arrange + int seededEdition = Version.currentBuildNumber() - 7; + SimpleFieldSet persisted = new SimpleFieldSet(true); + persisted.put("node.updater.lastKnownGoodFetchedEdition", seededEdition); + persisted.putSingle( + "node.updater.lastKnownGoodFetchedEditionKey", NodeUpdateManager.UPDATE_URI); + PersistentConfig persistedConfig = new PersistentConfig(persisted); + NodeUpdateManager seeded = new NodeUpdateManager(node, persistedConfig); + + // Act + seeded.startCoreUpdater(); + + // Assert + assertEquals( + Version.currentBuildNumber(), seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition()); + } + + @Test + void constructor_whenPersistedEditionKeyMismatched_expectEditionResetAndKeyCanonicalized() + throws Exception { + // Arrange + SimpleFieldSet persisted = new SimpleFieldSet(true); + persisted.put("node.updater.lastKnownGoodFetchedEdition", Version.currentBuildNumber() + 12); + persisted.putSingle( + "node.updater.lastKnownGoodFetchedEditionKey", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb,AQACAAE"); + PersistentConfig persistedConfig = new PersistentConfig(persisted); + + // Act + new NodeUpdateManager(node, persistedConfig); + + // Assert + assertEquals(-1, persistedConfig.get("node.updater").getInt("lastKnownGoodFetchedEdition")); + assertEquals( + NodeUpdateManager.UPDATE_URI, + persistedConfig.get("node.updater").getString("lastKnownGoodFetchedEditionKey")); + } + + @Test + void setURI_whenPublicKeyChanges_expectPersistedFetchedEditionReset() throws Exception { + // Arrange + int knownEdition = Version.currentBuildNumber() + 5; + manager.recordSuccessfulCoreInfoFetch( + manager.getCoreInfoURI().setSuggestedEdition(knownEdition), knownEdition); + String alternateKey = "v" + NodeUpdateManager.UPDATE_URI.substring(1); + FreenetURI changedKeyUri = + new FreenetURI("USK@" + alternateKey + "/info/" + Version.currentBuildNumber()); + + // Act + manager.setURI(changedKeyUri); + + // Assert + assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition")); + assertEquals( + alternateKey, config.get("node.updater").getString("lastKnownGoodFetchedEditionKey")); + } + + @Test + void recordSuccessfulCoreInfoFetch_whenMatchingKey_expectPersistedHintUpdated() { + // Arrange + int knownEdition = Version.currentBuildNumber() + 4; + + // Act + manager.recordSuccessfulCoreInfoFetch( + manager.getCoreInfoURI().setSuggestedEdition(knownEdition), knownEdition); + + // Assert + assertEquals(knownEdition, config.get("node.updater").getInt("lastKnownGoodFetchedEdition")); + assertEquals( + NodeUpdateManager.UPDATE_URI, + config.get("node.updater").getString("lastKnownGoodFetchedEditionKey")); + } + + @Test + void recordSuccessfulCoreInfoFetch_whenDifferentKey_expectPersistedHintUnchanged() + throws Exception { + // Arrange + String alternateKey = "v" + NodeUpdateManager.UPDATE_URI.substring(1); + FreenetURI changedKeyUri = + new FreenetURI("USK@" + alternateKey + "/info/" + Version.currentBuildNumber()); + + // Act + manager.recordSuccessfulCoreInfoFetch(changedKeyUri, Version.currentBuildNumber() + 4); + + // Assert + assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition")); + assertEquals( + NodeUpdateManager.UPDATE_URI, + config.get("node.updater").getString("lastKnownGoodFetchedEditionKey")); + } + @Test void getInstallerFiles_whenMissingOrPresent_expectNullOrFile() throws Exception { // Arrange: ensure a clean state From fd8f43189ae5ad36c2c5064991b732c2d8b71337 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:53:17 +0000 Subject: [PATCH 2/7] fix(updater): Guard null URI key extraction Add a null/empty guard in NodeUpdateManager.extractPublicKeyMaterial() before indexOf calls to avoid potential NullPointerException on malformed URI string rendering. Also replace duplicated revocation option literals with REVOCATION_URI_OPTION and rename a shadowing local test variable in NodeUpdateManagerTest. --- .../crypta/node/updater/NodeUpdateManager.java | 10 +++++++--- .../crypta/node/updater/NodeUpdateManagerTest.java | 12 +++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java index 4ee2cc7b8fa..87cce6c2a06 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java @@ -95,6 +95,7 @@ public final class NodeUpdateManager { "lastKnownGoodFetchedEdition"; private static final String LAST_KNOWN_GOOD_FETCHED_EDITION_KEY_OPTION = "lastKnownGoodFetchedEditionKey"; + private static final String REVOCATION_URI_OPTION = "revocationURI"; /** * The last build on the previous key with Java 7 support. Older nodes can update to this point @@ -260,7 +261,7 @@ public NodeUpdateManager(Node node, Config config) throws InvalidConfigValueExce } updaterConfig.register( - "revocationURI", + REVOCATION_URI_OPTION, REVOCATION_URI, new Option.Meta( 4, @@ -270,7 +271,7 @@ public NodeUpdateManager(Node node, Config config) throws InvalidConfigValueExce "NodeUpdateManager.revocationURILong"), new UpdateRevocationURICallback()); - String configuredRevocationUriValue = updaterConfig.getString("revocationURI"); + String configuredRevocationUriValue = updaterConfig.getString(REVOCATION_URI_OPTION); try { revocationURI = parseConfiguredRevocationURI(configuredRevocationUriValue); } catch (MalformedURLException e) { @@ -1330,7 +1331,7 @@ private void migrateLegacyRevocationUriValueIfNeeded( SubConfig updaterConfig, String configuredValue) throws InvalidConfigValueException { migrateLegacyOptionValueIfNeeded( updaterConfig, - "revocationURI", + REVOCATION_URI_OPTION, configuredValue, NodeUpdateManager::extractLegacyRevocationPublicKeyMaterial); } @@ -1364,6 +1365,9 @@ private static void migrateLegacyOptionValueIfNeeded( private static String extractPublicKeyMaterial(FreenetURI uri) { String fullUri = uri.toString(false, false); + if (fullUri == null || fullUri.isEmpty()) { + return ""; + } int typeSeparator = fullUri.indexOf(URI_TYPE_SEPARATOR); if (typeSeparator < 0) { return fullUri; diff --git a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java index b9bca50bd60..731571c1303 100644 --- a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java +++ b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java @@ -228,12 +228,12 @@ void constructor_whenLegacyFullUrisPersisted_expectAcceptedAndCanonicalizedToBar persisted.putSingle("node.updater.URI", "USK@" + NodeUpdateManager.UPDATE_URI + "/jar/1481"); persisted.putSingle( "node.updater.revocationURI", "SSK@" + NodeUpdateManager.REVOCATION_URI + "/revoked"); - PersistentConfig config = new PersistentConfig(persisted); + PersistentConfig persistedConfig = new PersistentConfig(persisted); // Act - NodeUpdateManager migrated = new NodeUpdateManager(node, config); + NodeUpdateManager migrated = new NodeUpdateManager(node, persistedConfig); - // Assert: updater URI uses current build and default info docname + // Assert: updater URI uses the current build and default info docname String expectedUpdate = "USK@" + NodeUpdateManager.UPDATE_URI + "/info/" + Version.currentBuildNumber(); assertEquals(expectedUpdate, migrated.getURI().toString(false, false)); @@ -242,9 +242,11 @@ void constructor_whenLegacyFullUrisPersisted_expectAcceptedAndCanonicalizedToBar migrated.getRevocationURI().toString(false, false)); // Assert: persisted option values are canonical bare keys after migration - assertEquals(NodeUpdateManager.UPDATE_URI, config.get("node.updater").getString("URI")); assertEquals( - NodeUpdateManager.REVOCATION_URI, config.get("node.updater").getString("revocationURI")); + NodeUpdateManager.UPDATE_URI, persistedConfig.get("node.updater").getString("URI")); + assertEquals( + NodeUpdateManager.REVOCATION_URI, + persistedConfig.get("node.updater").getString("revocationURI")); } @Test From 24ff0d7bd130fa3fea36d06b9d795b56452d9a6a Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:12:24 +0000 Subject: [PATCH 3/7] fix(updater): Preserve custom revocation URI values Keep full configured revocation URIs when they use a custom docname/path, while still serializing bare key material for the canonical /revoked form. Add regression tests to ensure callback/config round-trips do not rewrite custom revocation URI locations. --- .../node/updater/NodeUpdateManager.java | 9 +++++- .../node/updater/NodeUpdateManagerTest.java | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java index 87cce6c2a06..b3bd7143ee3 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java @@ -1233,7 +1233,7 @@ public UpdateRevocationURICallback() { @Override public String get() { - return extractPublicKeyMaterial(getRevocationURI()); + return toConfigRevocationUriValue(getRevocationURI()); } @Override @@ -1318,6 +1318,13 @@ private static String expandRevocationUriFromPublicKey(String keyMaterial) { return REVOCATION_URI_PREFIX + keyMaterial + URI_PATH_SEPARATOR + REVOCATION_URI_DOC_NAME; } + private static String toConfigRevocationUriValue(FreenetURI uri) { + if (uri.isSSK() && !uri.hasMetaStrings() && REVOCATION_URI_DOC_NAME.equals(uri.getDocName())) { + return extractPublicKeyMaterial(uri); + } + return uri.toString(false, false); + } + private void migrateLegacyUpdateUriValueIfNeeded(SubConfig updaterConfig, String configuredValue) throws InvalidConfigValueException { migrateLegacyOptionValueIfNeeded( diff --git a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java index 731571c1303..2d4a1352528 100644 --- a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java +++ b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java @@ -220,6 +220,20 @@ void uriCallbacks_get_expectPublicKeyOnly() { assertEquals(NodeUpdateManager.REVOCATION_URI, revocationCallback.get()); } + @Test + void revocationUriCallback_get_whenCustomDocName_expectFullUriPreserved() throws Exception { + // Arrange + NodeUpdateManager.UpdateRevocationURICallback revocationCallback = + manager.new UpdateRevocationURICallback(); + String customUri = "SSK@" + NodeUpdateManager.REVOCATION_URI + "/custom-revocation-doc"; + + // Act + manager.setRevocationURI(new FreenetURI(customUri)); + + // Assert + assertEquals(customUri, revocationCallback.get()); + } + @Test void constructor_whenLegacyFullUrisPersisted_expectAcceptedAndCanonicalizedToBareKeys() throws Exception { @@ -249,6 +263,23 @@ void constructor_whenLegacyFullUrisPersisted_expectAcceptedAndCanonicalizedToBar persistedConfig.get("node.updater").getString("revocationURI")); } + @Test + void constructor_whenCustomRevocationUriPersisted_expectCustomValuePreserved() throws Exception { + // Arrange + String customUri = "SSK@" + NodeUpdateManager.REVOCATION_URI + "/custom-revocation-doc"; + SimpleFieldSet persisted = new SimpleFieldSet(true); + persisted.putSingle("node.updater.URI", NodeUpdateManager.UPDATE_URI); + persisted.putSingle("node.updater.revocationURI", customUri); + PersistentConfig persistedConfig = new PersistentConfig(persisted); + + // Act + NodeUpdateManager custom = new NodeUpdateManager(node, persistedConfig); + + // Assert + assertEquals(customUri, custom.getRevocationURI().toString(false, false)); + assertEquals(customUri, persistedConfig.get("node.updater").getString("revocationURI")); + } + @Test void startCoreUpdater_whenMatchingPersistedEditionHigherThanCurrent_expectSubscribeSeedFromEdition() From 51bd6fd7238e39a219e2f3a7b736376de0ae3e0f Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:16:29 +0000 Subject: [PATCH 4/7] refactor(updater): remove legacy jar UOM paths Drop disabled main-jar UOM message handling and dead manager code paths, including unused legacy constants/callbacks in NodeUpdateManager. Remove unused inbound dependency handler state and simplify UOM dependency peer selection to announcement-derived peers. Update updater tests for new behavior and eliminate stale/unused test warnings. --- .../node/NodeControlMessageHandler.java | 31 +- .../node/updater/NodeUpdateManager.java | 24 - .../updater/UpdateOverMandatoryManager.java | 1308 ++--------------- .../crypta/node/updater/NodeUpdaterTest.java | 190 +++ .../UpdateOverMandatoryManagerTest.java | 30 +- 5 files changed, 308 insertions(+), 1275 deletions(-) create mode 100644 src/test/java/network/crypta/node/updater/NodeUpdaterTest.java diff --git a/src/main/java/network/crypta/node/NodeControlMessageHandler.java b/src/main/java/network/crypta/node/NodeControlMessageHandler.java index 6cad3fd2223..e7d0384dd02 100644 --- a/src/main/java/network/crypta/node/NodeControlMessageHandler.java +++ b/src/main/java/network/crypta/node/NodeControlMessageHandler.java @@ -8,7 +8,6 @@ import network.crypta.io.comm.MessageType; import network.crypta.io.comm.NotConnectedException; import network.crypta.io.comm.Peer; -import network.crypta.node.updater.NodeUpdateManager; import network.crypta.support.Fields; import network.crypta.support.ShortBuffer; import org.slf4j.Logger; @@ -49,7 +48,7 @@ final class NodeControlMessageHandler { private static final Logger LOG = LoggerFactory.getLogger(NodeControlMessageHandler.class); /** - * Owning node used to access network, messaging, and updater subsystems. + * The owning node used to access network, messaging, and updater subsystems. * *
This reference is stable for the lifetime of the handler and is not expected to be {@code * null}. It provides the necessary entry points for side effects triggered by control messages. @@ -144,7 +143,7 @@ private boolean handleDetectedAddress(Message m, PeerNode source, MessageType sp } /** - * Handles darknet visibility messages, if the source supports them. + * Handles darknet visibility messages if the source supports them. * * @param m message carrying visibility payload; must not be null * @param source peer that sent the message; must not be null @@ -229,23 +228,6 @@ private boolean handleUomMessages(Message m, PeerNode source, MessageType spec) .getUpdateOverMandatory() .handleSendingRevocation(m, source); } - if (Objects.equals(spec, DMT.CryptadUOMRequestMainJar) - && NodeUpdateManager.SUPPORTS_JAR_UOM - && source.isRealConnection()) { - node.services().nodeUpdater().getUpdateOverMandatory().handleRequestJar(m, source); - return true; - } - if (Objects.equals(spec, DMT.CryptadUOMSendingMainJar) - && NodeUpdateManager.SUPPORTS_JAR_UOM - && source.isRealConnection()) { - return node.services().nodeUpdater().getUpdateOverMandatory().handleSendingMain(m, source); - } - if (Objects.equals(spec, DMT.CryptadUOMFetchDependency) - && NodeUpdateManager.SUPPORTS_JAR_UOM - && source.isRealConnection()) { - node.services().nodeUpdater().getUpdateOverMandatory().handleFetchDependency(m, source); - return true; - } return false; } @@ -359,13 +341,14 @@ private void handleDisconnect(final Message m, final PeerNode source) { /** * Performs the actual disconnect workflow and processes parting metadata. * - * @param m original disconnect message containing flags and node-to-node data; must not be null + * @param m the original disconnect message containing flags and node-to-node data; must not be + * null * @param source peer being disconnected; must not be null */ private void finishDisconnect(final Message m, final PeerNode source) { source.disconnected(true, true); - // If true, remove from active routing table, likely to be down for a while. - // Otherwise, just dump all current connection state and keep trying to connect. + // If true, remove from the active routing table, likely to be down for a while. + // Otherwise, just dump all current connection states and keep trying to connect. boolean remove = m.getBoolean(DMT.REMOVE); if (remove) { node.network().peers().messenger().disconnectAndRemove(source, false, false, false); @@ -376,7 +359,7 @@ private void finishDisconnect(final Message m, final PeerNode source) { peerNode.getName()); } // If true, purge all references to this node. Otherwise, we can keep the node - // around in secondary tables etc. in order to more easily reconnect later. + // around in secondary tables etc. to more easily reconnect later. // (Mostly used on opennet) boolean purge = m.getBoolean(DMT.PURGE); if (purge) { diff --git a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java index b3bd7143ee3..dc75090236a 100644 --- a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java +++ b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java @@ -121,9 +121,6 @@ public final class NodeUpdateManager { /** Maximum on‑disk blob length in bytes for a persisted revocation document. */ public static final long MAX_REVOCATION_KEY_BLOB_LENGTH = 128L * 1024L; - /** Maximum allowed size in bytes for the historical main JAR (legacy paths only). */ - public static final long MAX_MAIN_JAR_LENGTH = 48L * 1024L * 1024L; // 48MiB - /** Maximum allowed size in bytes for the IPv4‐to‐country database. */ public static final long MAX_IP_TO_COUNTRY_LENGTH = 24L * 1024L * 1024L; @@ -133,15 +130,9 @@ public final class NodeUpdateManager { /** Remaining time for a legacy final-check timer. */ public static final long TIME_REMAINING_ON_CHECK = 0L; - /** Legacy timestamp for when normal main-jar fetching started. */ - static final long STARTED_FETCHING_NEXT_MAIN_JAR_TIMESTAMP = 0L; - /** Whether dependency checks are currently considered broken. */ public static final boolean BROKEN_DEPENDENCIES = false; - /** Whether legacy main-jar Update-over-Mandatory flows are enabled. */ - public static final boolean SUPPORTS_JAR_UOM = false; - // Installer/seednodes length caps removed with deprecated auto-fetch paths private FreenetURI updateURI; @@ -1541,21 +1532,6 @@ public void renderProgress(HTMLNode alertNode) { if (cu != null) cu.renderProperties(alertNode); } - /** Callback invoked when beginning a legacy UoM fetch; no‑op in package‑based mode. */ - public void onStartFetchingUOM() { - /* no-op */ - } - - /** - * Returns the legacy blob file for the current version. - * - * @return always {@code null}; serving the core JAR via UoM is disabled - */ - public synchronized File getCurrentVersionBlobFile() { - // Serving the main.jar over UOM is disabled in package-based updater. - return null; - } - // getMainUpdater() removed; jar updates are disabled. /** diff --git a/src/main/java/network/crypta/node/updater/UpdateOverMandatoryManager.java b/src/main/java/network/crypta/node/updater/UpdateOverMandatoryManager.java index 3bd139331a8..f8da24e5411 100644 --- a/src/main/java/network/crypta/node/updater/UpdateOverMandatoryManager.java +++ b/src/main/java/network/crypta/node/updater/UpdateOverMandatoryManager.java @@ -1,6 +1,5 @@ package network.crypta.node.updater; -import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; @@ -18,7 +17,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -61,23 +59,19 @@ import network.crypta.node.useralerts.AbstractUserAlert; import network.crypta.node.useralerts.UserAlert; import network.crypta.support.HTMLNode; -import network.crypta.support.HexUtil; import network.crypta.support.ShortBuffer; import network.crypta.support.SizeUtil; -import network.crypta.support.TimeUtil; import network.crypta.support.WeakHashSet; import network.crypta.support.api.Bucket; import network.crypta.support.api.RandomAccessBucket; import network.crypta.support.api.RandomAccessBuffer; import network.crypta.support.io.ArrayBucket; -import network.crypta.support.io.ByteArrayRandomAccessBuffer; import network.crypta.support.io.FileBucket; import network.crypta.support.io.FileRandomAccessBuffer; import network.crypta.support.io.FileUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -85,26 +79,20 @@ * *
UoM is a fallback update path used when peers are too far apart in protocol/build versions to * route requests normally. It piggybacks small control messages and bulk binary transfers so a node - * can receive critical information such as revocation certificates and, when enabled, a new core - * jar. The {@link network.crypta.node.NodeDispatcher} forwards UoM messages to this class, which - * decides whether and how to respond (accept, delay, or ignore) based on local policy and current - * update state managed by {@link NodeUpdateManager}. + * can receive critical information such as revocation certificates. The {@link + * network.crypta.node.NodeDispatcher} forwards UoM messages to this class, which decides whether + * and how to respond (accept, delay, or ignore) based on local policy and current update state + * managed by {@link NodeUpdateManager}. * - *
Behavior differs depending on the update mode. In the current package‑based updater flow - * (where {@link NodeUpdateManager#SUPPORTS_JAR_UOM} is {@code false}), UoM is only used for - * revocation handling; main‑jar exchange is disabled, and any received main‑jar offers are ignored - * after being logged for diagnostics. In legacy jar‑based mode, this manager may fetch a new jar - * directly from peers after a configurable grace period. + *
In the current package‑based updater flow, UoM is only used for revocation handling. Main‑jar + * exchange is disabled. * *
Concurrency and state: this class is designed to be called from network/async threads; it uses - * internal synchronization around its shared sets and maps to maintain consistency. At most {@link - * #MAX_NODES_SENDING_JAR} concurrent main‑jar transfers are allowed to reduce waste. A grace window - * ({@link #GRACE_TIME}) gives the normal updater time to succeed before UoM takes over. All state + * internal synchronization around its shared sets and maps to maintain consistency. All state * changes are defensive: inconsistent or malicious inputs are ignored and logged. * *
When a peer offers a newer main jar, the normal updater is given this much time before UoM
- * initiates a peer‑to‑peer fetch. The value is expressed in milliseconds and currently equals
- * three hours. Implementations should treat this as a read ‑only configuration; callers must not
- * modify it.
- */
- public static final long GRACE_TIME = HOURS.toMillis(3);
-
private static final String BUILD_NUM_PREFIX = " (build #";
private static final String NODE_PREFIX = "Node ";
private static final String PEER_PREFIX = "Peer ";
@@ -181,13 +144,7 @@ public class UpdateOverMandatoryManager implements RequestClient {
private static final Pattern revocationTempBuildNumberPattern =
Pattern.compile("^revocation(?:-jar)?-(\\d+-)?(\\d+)\\.fblob\\.tmp*$");
- // The main jar insert policy via UOM is handled elsewhere; no random insert here.
-
- private boolean fetchingUOM;
-
- private final HashMap The message may advertise a revocation certificate, a main‑jar offer (when jar UoM is
- * enabled), or both. Revocation announcements are processed first and may short‑circuit further
- * handling. For main‑jar offers, this method validates the advertised version and file length and
- * either fetches immediately (when outdated or past the grace time) or remembers the offer for a
- * later takeover.
+ * Announcements are used for revocation signaling and as a source of candidate peers for
+ * dependency fetchers. Revocation announcements are processed first and may short-circuit further
+ * handling.
*
- * @param m UOM announcement message to handle. Expected to contain keys such as {@code
- * MAIN_JAR_KEY}, {@code MAIN_JAR_VERSION}, and revocation fields; the map must be well‑formed
- * for correct processing.
+ * @param m UOM announcement message to handle.
* @param source The peer that sent the announcement. Must be a currently known {@link PeerNode};
* its connection status influences later scheduling.
* @return Always {@code true}. Returning a value allows symmetry with other handlers and aids
@@ -235,14 +182,11 @@ public UpdateOverMandatoryManager(NodeUpdateManager manager) {
*/
public boolean handleAnnounce(Message m, final PeerNode source) {
- String mainJarKey = m.getString(DMT.MAIN_JAR_KEY);
String revocationKey = m.getString(DMT.REVOCATION_KEY);
boolean haveRevocationKey = m.getBoolean(DMT.HAVE_REVOCATION_KEY);
- int mainJarVersion = m.getInt(DMT.MAIN_JAR_VERSION);
long revocationKeyLastTried = m.getLong(DMT.REVOCATION_KEY_TIME_LAST_TRIED);
int revocationKeyDNFs = m.getInt(DMT.REVOCATION_KEY_DNF_COUNT);
long revocationKeyFileLength = m.getLong(DMT.REVOCATION_KEY_FILE_LENGTH);
- long mainJarFileLength = m.getLong(DMT.MAIN_JAR_FILE_LENGTH);
int pingTime = m.getInt(DMT.PING_TIME);
int delayTime = m.getInt(DMT.BWLIMIT_DELAY_TIME);
@@ -253,8 +197,6 @@ public boolean handleAnnounce(Message m, final PeerNode source) {
"Update Over Mandatory offer from node {} : {}:",
source.getPeer(),
source.userToString());
- LOG.debug(
- "Main jar key: {} version={} length={}", mainJarKey, mainJarVersion, mainJarFileLength);
LOG.debug(
"Revocation key: {} found={} length={} last had 3 DNFs {} ms ago, {} DNFs so far",
revocationKey,
@@ -278,13 +220,10 @@ public boolean handleAnnounce(Message m, final PeerNode source) {
if (!stopProcessing) {
tellFetchers(source);
- // In package-based updater mode there is no main-jar UOM; only revocation is handled.
- if (!updateManager.isBlown()
- && updateManager.isEnabled()
- && NodeUpdateManager.SUPPORTS_JAR_UOM) {
- long now = System.currentTimeMillis();
- handleMainJarOffer(now, mainJarFileLength, mainJarVersion, source, mainJarKey);
+ synchronized (this) {
+ allNodesOfferedMainJar.add(source);
}
+ startSomeDependencyFetchers();
}
return true;
@@ -459,362 +398,6 @@ public void sent() {
}
- private void handleMainJarOffer(
- long now, long mainJarFileLength, int mainJarVersion, PeerNode source, String jarKey) {
-
- long started = NodeUpdateManager.STARTED_FETCHING_NEXT_MAIN_JAR_TIMESTAMP;
- // Legacy main-jar updater start time is not tracked in CoreUpdater mode; use the current offer
- // time.
- long whenToTakeOverTheNormalUpdater = now + GRACE_TIME;
- boolean isOutdated = updateManager.getNode().isOutdated();
- // if the new build is self-mandatory, or if the "normal" updater has been trying to update for
- // more than one hour
- if (LOG.isInfoEnabled()) {
- String takeoverDelay = TimeUtil.formatTime(whenToTakeOverTheNormalUpdater - now);
- LOG.info(
- "We received a valid UOMAnnouncement (main) : (isOutdated={} version={}"
- + " whenToTakeOverTheNormalUpdater={}) file length {} updateManager version {}",
- isOutdated,
- mainJarVersion,
- takeoverDelay,
- mainJarFileLength,
- updateManager.newMainJarVersion());
- }
-
- boolean offerIsValid =
- mainJarVersion > Version.currentBuildNumber()
- && mainJarFileLength > 0
- && mainJarVersion > updateManager.newMainJarVersion();
-
- if (offerIsValid) {
- source.setMainJarOfferedVersion(mainJarVersion);
- if (LOG.isDebugEnabled()) LOG.debug("Offer is valid");
- onValidMainJarOffer(
- now, started, isOutdated, mainJarVersion, source, jarKey, whenToTakeOverTheNormalUpdater);
- } else {
- // We may want the dependencies.
- // These may be similar even if his url is different, so add unconditionally.
- synchronized (this) {
- allNodesOfferedMainJar.add(source);
- }
- }
- startSomeDependencyFetchers();
- }
-
- private void onValidMainJarOffer(
- long now,
- long started,
- boolean isOutdated,
- int mainJarVersion,
- PeerNode source,
- String jarKey,
- long whenToTakeOverTheNormalUpdater) {
- if (isOutdated || whenToTakeOverTheNormalUpdater < now) {
- fetchMainJarNow(now, started, isOutdated, mainJarVersion, source, jarKey);
- } else {
- scheduleMainJarTakeover(whenToTakeOverTheNormalUpdater, now, source);
- }
- }
-
- private void fetchMainJarNow(
- long now,
- long started,
- boolean isOutdated,
- int mainJarVersion,
- PeerNode source,
- String jarKey) {
- // Take up the offer, subject to limits on the number of simultaneous downloads.
- // If we have fetches running already, then sendUOMRequestMainJar() will add the offer to
- // nodesOfferedMainJar, so that if all our fetches fail, we can fetch from this node.
- if (!isOutdated) {
- String howLong = TimeUtil.formatTime(now - started);
- LOG.error(
- "The update process seems to have been stuck for {}; let's switch to UoM! SHOULD NOT"
- + " HAPPEN! (1)",
- howLong);
- } else if (LOG.isDebugEnabled()) {
- LOG.debug("Fetching via UOM as our build is deprecated");
- }
- try {
- FreenetURI mainJarURI = new FreenetURI(jarKey).setSuggestedEdition(mainJarVersion);
- if (mainJarURI.equals(updateManager.getURI().setSuggestedEdition(mainJarVersion))) {
- sendUOMRequest(source, true);
- } else {
- // Transitional version differences may be expected; logging retained for diagnostics.
- if (LOG.isWarnEnabled()) {
- LOG.warn(
- "{}{} offered us a new main jar (version {}) but key differs. our key: {} his key:{}",
- NODE_PREFIX,
- source.userToString(),
- mainJarVersion,
- updateManager.getURI(),
- mainJarURI);
- }
- }
- } catch (MalformedURLException e) {
- // Should maybe be a useralert?
- LOG.error(
- "Node {} sent us a UOMAnnouncement claiming to have a new ext jar, but it had an invalid"
- + " URI: {}",
- source,
- jarKey,
- e);
- }
- synchronized (this) {
- allNodesOfferedMainJar.add(source);
- }
- }
-
- private void scheduleMainJarTakeover(
- long whenToTakeOverTheNormalUpdater, long now, final PeerNode source) {
- // Don't take up the offer. Add to nodesOfferedMainJar so that we know where to fetch it
- // from when we need it.
- synchronized (this) {
- nodesOfferedMainJar.add(source);
- allNodesOfferedMainJar.add(source);
- }
- updateManager
- .getNode()
- .network()
- .ticker()
- .queueTimedJob(
- () -> {
- if (updateManager.isBlown()) return;
- if (!updateManager.isEnabled()) return;
- if (updateManager.hasNewMainJar()) return;
- if (!updateManager.getNode().isOutdated()) {
- LOG.error(
- "The update process seems to have been stuck for too long; let's switch to UoM!"
- + " SHOULD NOT HAPPEN! (2) (ext)");
- }
- maybeRequestMainJar();
- },
- whenToTakeOverTheNormalUpdater - now);
- }
-
- private void sendUOMRequest(final PeerNode source, boolean addOnFail) {
- final String name = "Main";
- String lname = "main";
- if (LOG.isDebugEnabled()) LOG.debug("sendUOMRequest {} ({},{})", name, source, addOnFail);
- if (!source.isConnected() || source.isSeed()) {
- if (LOG.isDebugEnabled())
- LOG.debug("Not sending UOM {} request to {} (disconnected or seednode)", lname, source);
- return;
- }
- final Set Respects {@link #MAX_NODES_SENDING_JAR}. If no capacity is available or no offers remain,
- * the method returns immediately. This method is idempotent with respect to already contacted
- * peers.
- */
- protected void maybeRequestMainJar() {
- PeerNode[] offers;
- synchronized (this) {
- if (nodesAskedSendMainJar.size() + nodesSendingMainJar.size() >= MAX_NODES_SENDING_JAR)
- return;
- if (nodesOfferedMainJar.isEmpty()) return;
- offers = nodesOfferedMainJar.toArray(new PeerNode[0]);
- }
- for (PeerNode offer : offers) {
- boolean shouldSkip;
- synchronized (this) {
- if (nodesAskedSendMainJar.size() + nodesSendingMainJar.size() >= MAX_NODES_SENDING_JAR)
- return;
- shouldSkip = nodesSendingMainJar.contains(offer) || nodesAskedSendMainJar.contains(offer);
- }
- if (!offer.isConnected() || shouldSkip) continue;
- sendUOMRequest(offer, false);
- }
- }
-
private void alertUser() {
synchronized (this) {
if (alert != null) return;
@@ -1624,13 +1207,6 @@ private void cancelSend(PeerNode source, long uid) {
}
}
- private void removeAskedAndCancel(PeerNode source, long uid) {
- cancelSend(source, uid);
- synchronized (this) {
- this.nodesAskedSendMainJar.remove(source);
- }
- }
-
/**
* Unregisters and clears the current “peers say key blown” alert, if any.
*
@@ -1645,602 +1221,106 @@ public void killAlert() {
}
}
+ // Removed an unused method maybeInsertMainJar: insertion is coordinated via updater flows.
+
/**
- * Handles a peer request to send the current main jar binary blob.
- *
- * Validates policy (e.g., opennet restrictions and minimum peer version), locates the local
- * jar, and initiates a bulk transfer back to the requester. When requirements are not met or the
- * jar is unavailable, the method logs and returns without transferring.
+ * Deletes obsolete persistent temporary files related to UoM transfers.
*
- * @param m Request message with a unique {@code UID} to correlate the bulk transfer.
- * @param source The requesting peer; connection state and version determine eligibility.
+ * The method scans the persistent temp directory for known UoM patterns (revocation and
+ * main‑jar blobs and their temporary variants). It removes files that are clearly safe to delete,
+ * including old build‑number‑scoped files below the minimum acceptable build. Errors are logged
+ * but otherwise ignored.
*/
- public void handleRequestJar(Message m, final PeerNode source) {
- final String name = "main";
-
- Message msg;
-
- if (source.isOpennet() && updateManager.dontAllowUOM()) {
- LOG.info(
- "Peer {} asked us for the blob file for {}; We are a seednode, so we ignore it!",
- source,
- name);
+ protected void removeOldTempFiles() {
+ File oldTempFilesPeerDir =
+ updateManager.getNode().services().clientCore().getPersistentTempDir();
+ if (!oldTempFilesPeerDir.exists()) return;
+ if (!oldTempFilesPeerDir.isDirectory()) {
+ LOG.error(
+ "Persistent temporary files location is not a directory: {}",
+ oldTempFilesPeerDir.getPath());
return;
}
- // Do we have the data?
- File data;
- int version;
- FreenetURI uri;
- // Legacy support removed - only serve the current version
- if (!Version.isBuildAtLeast(
- source.getNodeName(), source.getBuildNumber(), NodeUpdateManager.TRANSITION_VERSION)) {
- // Don't serve updates to very old nodes
- LOG.info(
- "Peer {} is too old (version < {}), not serving update",
- source,
- NodeUpdateManager.TRANSITION_VERSION);
+ // Best-effort cleanup; failures are only logged.
+ File[] oldTempFiles =
+ oldTempFilesPeerDir.listFiles(file -> shouldDeleteTempFile(file.getName()));
+ if (oldTempFiles == null) {
+ LOG.warn("Could not list temporary persistent files in {}", oldTempFilesPeerDir);
return;
}
- data = updateManager.getCurrentVersionBlobFile();
- version = Version.currentBuildNumber();
- uri = updateManager.getURI();
- if (data == null) {
- LOG.info(
- "UOM main jar request: peer {} requested {} jar but it is missing locally", source, name);
- // Probably a race condition on reconnection, hopefully we'll be asked again
- return;
+ for (File fileToDelete : oldTempFiles) {
+ String fileToDeleteName = fileToDelete.getName();
+ try {
+ Files.delete(fileToDelete.toPath());
+ } catch (NoSuchFileException _) {
+ LOG.info("Temporary persistent file does not exist when deleting: {}", fileToDeleteName);
+ } catch (IOException _) {
+ LOG.error(
+ "Cannot delete temporary persistent file {} even though it exists: must be TOO"
+ + " persistent :)",
+ fileToDeleteName);
+ }
}
- final long uid = m.getLong(DMT.UID);
-
- if (!source.sendingUOMJar(false)) {
- LOG.error("Peer {} asked for UOM main jar twice", source);
- return;
- }
+ // Caller doesn't use the result; nothing to return.
+ }
- final long length = data.length();
- msg = DMT.createUOMSendingMainJar(uid, length, uri.toString(), version);
+ private boolean shouldDeleteTempFile(String fileName) {
+ if (fileName.startsWith("revocation-") && fileName.endsWith(FBLOB_TMP_SUFFIX)) return true;
- final Runnable r = buildMainJarSender(source, data, uid, length);
+ Matcher mainBuildNumberMatcher = mainBuildNumberPattern.matcher(fileName);
+ Matcher mainTempBuildNumberMatcher = mainTempBuildNumberPattern.matcher(fileName);
+ Matcher revocationTempBuildNumberMatcher = revocationTempBuildNumberPattern.matcher(fileName);
- try {
- source
- .transport()
- .sendAsync(
- msg,
- new AsyncMessageCallback() {
+ if (mainBuildNumberMatcher.matches()) {
+ try {
+ String buildNumberStr = mainBuildNumberMatcher.group(1);
+ int buildNumber = Integer.parseInt(buildNumberStr);
+ int lastGoodMainBuildNumber = Version.MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER;
+ return buildNumber < lastGoodMainBuildNumber;
+ } catch (NumberFormatException _) {
+ LOG.error("Wierd file in persistent temp: {}", fileName);
+ return false;
+ }
+ }
+ return mainTempBuildNumberMatcher.matches() || revocationTempBuildNumberMatcher.matches();
+ }
- @Override
- public void acknowledged() {
- if (LOG.isDebugEnabled()) LOG.debug("UOM main jar send: starting data transfer");
- // Send the data
+ /** {@inheritDoc} */
+ @Override
+ public boolean persistent() {
+ return false;
+ }
- updateManager
- .getNode()
- .network()
- .executor()
- .execute(r, name + " jar send for " + uid + " to " + source.userToString());
- }
-
- @Override
- public void disconnected() {
- // Argh
- LOG.error(
- "UOM main jar send aborted: peer {} disconnected before UOMSendingMainJar for"
- + " {} jar",
- source,
- name);
- source.finishedSendingUOMJar(false);
- }
-
- @Override
- public void fatalError() {
- // Argh
- LOG.error(
- "UOM main jar send failed: fatal error before UOMSendingMainJar from peer {}"
- + " for {} jar",
- source,
- name);
- source.finishedSendingUOMJar(false);
- }
-
- @Override
- public void sent() {
- if (LOG.isDebugEnabled())
- LOG.debug("UOM main jar send: message sent, data follows");
- }
-
- @Override
- public String toString() {
- return super.toString() + "(" + uid + ":" + source.getPeer() + ")";
- }
- },
- updateManager.getByteCounter());
- } catch (NotConnectedException e) {
- LOG.error(
- "UOM main jar send failed: peer {} disconnected while sending UOMSendingMainJar for {}"
- + " jar",
- source,
- name,
- e);
- } catch (RuntimeException e) {
- source.finishedSendingUOMJar(false);
- throw e;
- }
- }
-
- private Runnable buildMainJarSender(
- final PeerNode source, final File data, final long uid, final long length) {
- return () -> {
- try (FileRandomAccessBuffer rafLocal = new FileRandomAccessBuffer(data, true)) {
- PartiallyReceivedBulk prb =
- new PartiallyReceivedBulk(
- updateManager.getNode().network().usm(), length, Node.PACKET_SIZE, rafLocal, true);
- BulkTransmitter bt =
- new BulkTransmitter(prb, source, uid, false, updateManager.getByteCounter(), true);
- if (!bt.send()) {
- if (LOG.isErrorEnabled()) {
- LOG.error(
- "Failed to send {} jar blob to {} : {}",
- "main",
- source.userToString(),
- bt.getCancelReason());
- }
- } else {
- if (LOG.isInfoEnabled()) {
- LOG.info("Sent {} jar blob to {}", "main", source.userToString());
- }
- }
- } catch (FileNotFoundException e) {
- LOG.error("UOM main jar send failed: missing file after check for {} jar", "main", e);
- } catch (IOException e) {
- LOG.error("UOM main jar send failed: disk I/O reading {} jar after download", "main", e);
- } catch (DisconnectedException e) {
- LOG.error(
- "UOM main jar send failed: peer {} disconnected during bulk send for {} jar",
- source,
- "main",
- e);
- } finally {
- source.finishedSendingUOMJar(false);
- }
- };
- }
+ /**
+ * Clears UoM state associated with a disconnected peer.
+ *
+ * Removes the peer from all tracking sets (offers, active transfers, and revocation reports)
+ * and re‑evaluates whether the revocation condition still plausibly holds.
+ *
+ * @param pn The peer that disconnected.
+ */
+ public void disconnected(PeerNode pn) {
+ synchronized (this) {
+ nodesSayKeyRevoked.remove(pn);
+ nodesSayKeyRevokedFailedTransfer.remove(pn);
+ nodesSayKeyRevokedTransferring.remove(pn);
+ allNodesOfferedMainJar.remove(pn);
+ }
+ maybeNotRevoked();
+ }
/**
- * Handles a peer announcement that it is sending the main jar to us.
+ * Reports whether at least two UoM transfers are in progress.
*
- * Validates the advertised {@code URI}, version, and length, checks local acceptance rules,
- * and, if acceptable, schedules a bulk receiving to a temporary file followed by verification and
- * cleanup. If the offer is rejected, the method cancels the transfer.
+ * Main-jar UoM is disabled, so this always returns {@code false}.
*
- * @param m Message describing the transfer, including {@code UID}, {@code FILE_LENGTH}, {@code
- * MAIN_JAR_KEY}, and {@code MAIN_JAR_VERSION}.
- * @param source The peer that will transmit the jar.
- * @return {@code true} when the message was handled; {@code false} is not used.
- */
- public boolean handleSendingMain(Message m, final PeerNode source) {
- final long uid = m.getLong(DMT.UID);
- final long length = m.getLong(DMT.FILE_LENGTH);
- final String key = m.getString(DMT.MAIN_JAR_KEY);
- final int version = m.getInt(DMT.MAIN_JAR_VERSION);
- processSendingMain(uid, length, key, version, source);
- return true;
- }
-
- private void processSendingMain(long uid, long length, String key, int version, PeerNode source) {
- final FreenetURI jarURI;
- try {
- jarURI = new FreenetURI(key).setSuggestedEdition(version);
- } catch (MalformedURLException e) {
- LOG.error(
- "Failed receiving main jar {} because URI not parsable: {} for {}", version, e, key);
- removeAskedAndCancel(source, uid);
- return;
- }
-
- if (!isUriAccepted(jarURI, version, source, uid)) return;
- if (!canReceiveMainJar(source, uid)) return;
- if (!isMainJarLengthAccepted(length, version, source, uid)) return;
-
- if (LOG.isInfoEnabled()) {
- LOG.info("Receiving main jar {}{}{}", version, FROM_LITERAL, source.userToString());
- }
-
- final File temp = prepareTempMainJarFile(uid, source);
- if (temp == null) return;
-
- FileRandomAccessBuffer raf = prepareMainJarRaf(temp, length, source);
- if (raf == null) return;
-
- PartiallyReceivedBulk prb =
- new PartiallyReceivedBulk(
- updateManager.getNode().network().usm(), length, Node.PACKET_SIZE, raf, false);
-
- final BulkReceiver br = new BulkReceiver(prb, source, uid, updateManager.getByteCounter());
- final FreenetURI jarUriForLambda = jarURI;
- updateManager
- .getNode()
- .network()
- .executor()
- .execute(
- () -> {
- boolean success = false;
- try {
- synchronized (UpdateOverMandatoryManager.class) {
- nodesAskedSendMainJar.remove(source);
- nodesSendingMainJar.add(source);
- }
- success = br.receive();
- if (success) processMainJarBlob(temp, source, version, jarUriForLambda);
- else {
- LOG.error("Failed to transfer main jar {}{}{}", version, FROM_LITERAL, source);
- try {
- Files.delete(temp.toPath());
- } catch (IOException ex) {
- LOG.warn(FAILED_DELETE_TMP, temp, ex);
- }
- }
- } finally {
- synchronized (UpdateOverMandatoryManager.class) {
- nodesSendingMainJar.remove(source);
- if (success) nodesSentMainJar.add(source);
- }
- }
- },
- "Main jar ("
- + version
- + ") receive"
- + FOR_LITERAL
- + uid
- + FROM_LITERAL
- + source.userToString());
- }
-
- private boolean isUriAccepted(FreenetURI jarURI, int version, PeerNode source, long uid) {
- if (!jarURI.equals(updateManager.getURI().setSuggestedEdition(version))) {
- if (LOG.isWarnEnabled()) {
- LOG.warn(
- """
- Node sending us a main jar update ({}) from the wrong URI:
- Node: {}
- Our URI: {}
- Their URI: {}
- """,
- version,
- source.userToString(),
- updateManager.getURI(),
- jarURI);
- }
- removeAskedAndCancel(source, uid);
- return false;
- }
- return true;
- }
-
- private boolean canReceiveMainJar(PeerNode source, long uid) {
- if (updateManager.isBlown()) {
- LOG.debug("Key blown, so not receiving main jar from {}({})", source, uid);
- removeAskedAndCancel(source, uid);
- return false;
- }
- return true;
- }
-
- private boolean isMainJarLengthAccepted(long length, int version, PeerNode source, long uid) {
- if (length > NodeUpdateManager.MAX_MAIN_JAR_LENGTH) {
- if (LOG.isErrorEnabled()) {
- LOG.error(
- "{}{} offered us a main jar ({}) {} long. This is unacceptably long so we have refused"
- + " the transfer.",
- NODE_PREFIX,
- source.userToString(),
- version,
- SizeUtil.formatSize(length));
- LOG.error(
- "Node {} offered us a main jar ({}) {} long. This is unacceptably long so we have"
- + " refused the transfer.",
- source.userToString(),
- version,
- SizeUtil.formatSize(length));
- }
- // If the transfer fails, we don't try again.
- removeAskedAndCancel(source, uid);
- return false;
- }
- return true;
- }
-
- private File prepareTempMainJarFile(long uid, PeerNode source) {
- try {
- File temp =
- File.createTempFile(
- "main-",
- FBLOB_TMP_SUFFIX,
- updateManager.getNode().services().clientCore().getPersistentTempDir());
- temp.deleteOnExit();
- return temp;
- } catch (IOException e) {
- LOG.error("Cannot save new main jar to disk and therefore cannot fetch it from our peer!", e);
- removeAskedAndCancel(source, uid);
- return null;
- }
- }
-
- private FileRandomAccessBuffer prepareMainJarRaf(File temp, long length, PeerNode source) {
- try {
- return new FileRandomAccessBuffer(temp, length, false);
- } catch (IOException e) {
- LOG.error(
- "Peer {} sending us a main jar binary blob, but we {}{} : {}",
- source,
- (e instanceof FileNotFoundException)
- ? "lost the temp file "
- : "cannot read the temp file ",
- temp,
- e,
- e);
- synchronized (this) {
- this.nodesAskedSendMainJar.remove(source);
- }
- return null;
- }
- }
-
- /**
- * Verifies and processes a received main‑jar binary blob.
- *
- * Reads the blob, reconstructs a temporary fetch context using its blocks, and fetches the jar
- * via the local store using the supplied {@code uri}. On success, the cleaned blob is freed, and
- * the temporary file is deleted; failures are logged and the temp file is removed.
- *
- * @param temp Temporary file containing the binary blob as received over UoM; must exist.
- * @param source Peer that sent the blob; used for logs, may be {@code null} for local testing.
- * @param version Suggested edition to fetch; must be positive.
- * @param uri Expected URI of the jar to fetch from the store for validation and assembly.
- */
- protected void processMainJarBlob(
- final File temp, final PeerNode source, final long version, FreenetURI uri) {
- SimpleBlockSet blocks = new SimpleBlockSet();
- final String toString = source == null ? "(local)" : source.userToString();
-
- if (!readMainJarBlob(temp, version, toString, blocks)) return;
-
- // Fetch the jar from the datastore plus the binary blob
-
- FetchContext seedContext =
- updateManager
- .getNode()
- .services()
- .clientCore()
- .makeClient((short) 0, true, false)
- .getFetchContext();
- FetchContext tempContext =
- new FetchContext(seedContext, FetchContext.IDENTICAL_MASK, true, blocks);
- tempContext.setLocalRequestOnly(true);
-
- final ArrayBucket cleanedBlob = new ArrayBucket();
-
- ClientGetCallback myCallback = buildMainJarCallback(temp, version, toString, cleanedBlob);
-
- ClientGetter cg =
- new ClientGetter(
- myCallback, uri, tempContext, (short) 0, null, new BinaryBlobWriter(cleanedBlob), null);
-
- try {
- updateManager.getNode().services().clientCore().getClientContext().start(cg);
- } catch (FetchException e1) {
- myCallback.onFailure(e1);
- } catch (PersistenceDisabledException _) {
- // Impossible
- }
- }
-
- private boolean readMainJarBlob(File temp, long version, String toString, SimpleBlockSet blocks) {
- try (DataInputStream dis =
- new DataInputStream(new BufferedInputStream(new FileInputStream(temp)))) {
- BinaryBlob.readBinaryBlob(dis, blocks, true);
- return true;
- } catch (FileNotFoundException _) {
- LOG.error(
- "{}{} ? We lost the main jar ({}) from {}!",
- SOMEONE_DELETED_PREFIX,
- temp,
- version,
- toString);
- return false;
- } catch (IOException _) {
- LOG.error(
- "Could not read main jar ({}) from temp file {} from node {} !", version, temp, toString);
- return false;
- } catch (BinaryBlobFormatException e) {
- LOG.error("Peer {} sent us an invalid main jar ({})!", toString, version, e);
- return false;
- }
- }
-
- // No file-backed cleaned blob is required for UOM; we buffer in memory.
-
- private ClientGetCallback buildMainJarCallback(
- final File temp, final long version, final String toString, final Bucket cleanedBlob) {
- return new ClientGetCallback() {
-
- @Override
- public void onFailure(FetchException e) {
- handleMainJarFetchFailure(e, temp, version, toString, cleanedBlob);
- }
-
- @Override
- public void onSuccess(FetchResult result, ClientGetter state) {
- LOG.info("Got main jar version {}{}{}", version, FROM_LITERAL, toString);
- if (result.size() == 0) {
- LOG.warn("Ignoring main jar because 0 bytes long");
- return;
- }
-
- if (!NodeUpdateManager.SUPPORTS_JAR_UOM) {
- LOG.info("Ignoring UOM main jar because jar updates are disabled.");
- try {
- Files.delete(temp.toPath());
- } catch (IOException ex) {
- LOG.warn(FAILED_DELETE_TMP, temp, ex);
- }
- if (cleanedBlob != null) cleanedBlob.free();
- return;
- }
- if (cleanedBlob != null) cleanedBlob.free();
- }
-
- @Override
- public void onResume(ClientContext context) {
- // Not persistent.
- }
-
- @Override
- public RequestClient getRequestClient() {
- return UpdateOverMandatoryManager.this;
- }
- };
- }
-
- private void handleMainJarFetchFailure(
- FetchException e, File temp, long version, String toString, Bucket cleanedBlob) {
- if (e.mode == FetchExceptionMode.CANCELLED) {
- LOG.error("Cancelled fetch from store/blob of main jar ({}) from {}", version, toString);
- } else if (e.newURI != null) {
- try {
- Files.delete(temp.toPath());
- } catch (IOException ex) {
- LOG.warn(FAILED_DELETE_TMP, temp, ex);
- }
- LOG.error("URI changed fetching main jar {} from {}", version, toString);
- } else if (e.isFatal()) {
- try {
- Files.delete(temp.toPath());
- } catch (IOException ex) {
- LOG.warn(FAILED_DELETE_TMP, temp, ex);
- }
- LOG.error(
- "Failed to fetch main jar {} from {} : fatal error (update was probably inserted badly):",
- version,
- toString,
- e);
- } else {
- LOG.error("Failed to fetch main jar {} from blob from {}", version, toString);
- }
- if (cleanedBlob != null) cleanedBlob.free();
- }
-
- // Removed an unused method maybeInsertMainJar: insertion is coordinated via updater flows.
-
- /**
- * Deletes obsolete persistent temporary files related to UoM transfers.
- *
- * The method scans the persistent temp directory for known UoM patterns (revocation and
- * main‑jar blobs and their temporary variants). It removes files that are clearly safe to delete,
- * including old build‑number‑scoped files below the minimum acceptable build. Errors are logged
- * but otherwise ignored.
- */
- protected void removeOldTempFiles() {
- File oldTempFilesPeerDir =
- updateManager.getNode().services().clientCore().getPersistentTempDir();
- if (!oldTempFilesPeerDir.exists()) return;
- if (!oldTempFilesPeerDir.isDirectory()) {
- LOG.error(
- "Persistent temporary files location is not a directory: {}",
- oldTempFilesPeerDir.getPath());
- return;
- }
-
- // Best-effort cleanup; failures are only logged.
- File[] oldTempFiles =
- oldTempFilesPeerDir.listFiles(file -> shouldDeleteTempFile(file.getName()));
- if (oldTempFiles == null) {
- LOG.warn("Could not list temporary persistent files in {}", oldTempFilesPeerDir);
- return;
- }
-
- for (File fileToDelete : oldTempFiles) {
- String fileToDeleteName = fileToDelete.getName();
- try {
- Files.delete(fileToDelete.toPath());
- } catch (NoSuchFileException _) {
- LOG.info("Temporary persistent file does not exist when deleting: {}", fileToDeleteName);
- } catch (IOException _) {
- LOG.error(
- "Cannot delete temporary persistent file {} even though it exists: must be TOO"
- + " persistent :)",
- fileToDeleteName);
- }
- }
-
- // Caller doesn't use the result; nothing to return.
- }
-
- private boolean shouldDeleteTempFile(String fileName) {
- if (fileName.startsWith("revocation-") && fileName.endsWith(FBLOB_TMP_SUFFIX)) return true;
-
- Matcher mainBuildNumberMatcher = mainBuildNumberPattern.matcher(fileName);
- Matcher mainTempBuildNumberMatcher = mainTempBuildNumberPattern.matcher(fileName);
- Matcher revocationTempBuildNumberMatcher = revocationTempBuildNumberPattern.matcher(fileName);
-
- if (mainBuildNumberMatcher.matches()) {
- try {
- String buildNumberStr = mainBuildNumberMatcher.group(1);
- int buildNumber = Integer.parseInt(buildNumberStr);
- int lastGoodMainBuildNumber = Version.MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER;
- return buildNumber < lastGoodMainBuildNumber;
- } catch (NumberFormatException _) {
- LOG.error("Wierd file in persistent temp: {}", fileName);
- return false;
- }
- }
- return mainTempBuildNumberMatcher.matches() || revocationTempBuildNumberMatcher.matches();
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean persistent() {
- return false;
- }
-
- /**
- * Clears UoM state associated with a disconnected peer.
- *
- * Removes the peer from all tracking sets (offers, active transfers, and revocation reports)
- * and re‑evaluates whether the revocation condition still plausibly holds.
- *
- * @param pn The peer that disconnected.
- */
- public void disconnected(PeerNode pn) {
- synchronized (this) {
- nodesSayKeyRevoked.remove(pn);
- nodesSayKeyRevokedFailedTransfer.remove(pn);
- nodesSayKeyRevokedTransferring.remove(pn);
- nodesOfferedMainJar.remove(pn);
- allNodesOfferedMainJar.remove(pn);
- nodesSentMainJar.remove(pn);
- nodesAskedSendMainJar.remove(pn);
- nodesSendingMainJar.remove(pn);
- }
- maybeNotRevoked();
- }
-
- /**
- * Reports whether two concurrent main‑jar transfers are in progress.
- *
- * This reflects the internal cap enforced by {@link #MAX_NODES_SENDING_JAR} for reliability
- * and bandwidth conservation.
- *
- * @return {@code true} if at least two peers are currently sending the main jar; otherwise {@code
- * false}.
+ * @return {@code false}
*/
public boolean fetchingFromTwo() {
- synchronized (this) {
- return this.nodesSendingMainJar.size() >= 2;
- }
+ return false;
}
/** {@inheritDoc} */
@@ -2250,190 +1330,14 @@ public boolean realTimeFlag() {
}
/**
- * Indicates whether a main‑jar transfer is currently active.
+ * Indicates whether a legacy main-jar UoM transfer is active.
*
- * @return {@code true} if at least one peer is sending the main jar; {@code false} otherwise.
- */
- public boolean isFetchingMain() {
- synchronized (this) {
- return !nodesSendingMainJar.isEmpty();
- }
- }
-
- /**
- * Advertises a locally available dependency that can be served to peers by hash.
+ * Main-jar UoM is disabled, so this always returns {@code false}.
*
- * The file is indexed by its {@code SHA‑256} hash and may later be read and transferred on
- * demand. Only register files that currently exist and are readable; absence at transfer time is
- * treated as a transient failure.
- *
- * @param expectedHash The exact SHA‑256 of the file content as a byte array; must not be null.
- * @param filename The on‑disk file to serve when requested; the path is not copied.
+ * @return {@code false}
*/
- @SuppressWarnings("unused")
- public void addDependency(byte[] expectedHash, File filename) {
- if (LOG.isDebugEnabled())
- LOG.debug("Add dependency: {} for {}", filename, HexUtil.bytesToHex(expectedHash));
- synchronized (dependencies) {
- dependencies.put(new ShortBuffer(expectedHash), filename);
- }
- }
-
- static final int MAX_TRANSFERS_PER_PEER = 2;
-
- /**
- * Handles a peer request to fetch a registered dependency by its hash.
- *
- * Validates the request, enforces per‑peer transfer caps, and streams the file using the bulk
- * transfer protocol when available. If the file is unavailable or the request exceeds limits, a
- * cancellation is sent instead.
- *
- * @param m The request message containing {@code EXPECTED_HASH}, {@code FILE_LENGTH}, and {@code
- * UID} fields.
- * @param source The requesting peer.
- */
- public void handleFetchDependency(Message m, final PeerNode source) {
- File data;
- final ShortBuffer buf = (ShortBuffer) m.getObject(DMT.EXPECTED_HASH);
- long length = m.getLong(DMT.FILE_LENGTH);
- long uid = m.getLong(DMT.UID);
- synchronized (dependencies) {
- data = dependencies.get(buf);
- }
- boolean fail = !incrementDependencies(source);
- FileRandomAccessBuffer raf;
- final BulkTransmitter bt;
-
- DepOpen dep = openDependency(data, buf, source);
- raf = dep.raf;
- fail = fail || dep.fail;
-
- PrbDecision prbDecision = buildDependencyPrb(raf, length);
- PartiallyReceivedBulk prb = prbDecision.prb();
- fail = fail || prbDecision.sizeMismatch();
-
- try {
- bt = new BulkTransmitter(prb, source, uid, false, updateManager.getByteCounter(), true);
- } catch (DisconnectedException e) {
- LOG.error(
- "Peer {} asked us for the dependency with hash {} jar then disconnected",
- source,
- HexUtil.bytesToHex(buf.getData()),
- e);
- if (raf != null) {
- raf.close();
- }
- decrementDependencies(source);
- return;
- }
-
- if (fail) {
- cancelSend(source, uid);
- decrementDependencies(source);
- } else {
- final FileRandomAccessBuffer r = raf;
- updateManager
- .getNode()
- .network()
- .executor()
- .execute(
- () -> {
- source.incrementUOMSends();
- try {
- bt.send();
- } catch (DisconnectedException _) {
- LOG.info(
- "Disconnected while sending dependency with hash {} to {}",
- HexUtil.bytesToHex(buf.getData()),
- source);
- } finally {
- source.decrementUOMSends();
- decrementDependencies(source);
- if (r != null) {
- r.close();
- }
- }
- });
- }
- }
-
- private record PrbDecision(PartiallyReceivedBulk prb, boolean sizeMismatch) {}
-
- private PrbDecision buildDependencyPrb(FileRandomAccessBuffer raf, long expectedLength) {
- if (raf != null) {
- long thisLength = raf.size();
- PartiallyReceivedBulk prb =
- new PartiallyReceivedBulk(
- updateManager.getNode().network().usm(), thisLength, Node.PACKET_SIZE, raf, true);
- return new PrbDecision(prb, expectedLength != thisLength);
- }
- PartiallyReceivedBulk prb =
- new PartiallyReceivedBulk(
- updateManager.getNode().network().usm(),
- 0,
- Node.PACKET_SIZE,
- new ByteArrayRandomAccessBuffer(new byte[0]),
- true);
- return new PrbDecision(prb, false);
- }
-
- private void decrementDependencies(PeerNode source) {
- synchronized (peersFetchingDependencies) {
- Integer x = peersFetchingDependencies.get(source);
- if (x == null) {
- LOG.error("Inconsistent dependency counting? Should not be null for {}", source);
- } else if (x == 1) {
- peersFetchingDependencies.remove(source);
- } else if (x <= 0) {
- LOG.error("Inconsistent dependency counting? Counter is {} for {}", x, source);
- peersFetchingDependencies.remove(source);
- } else {
- peersFetchingDependencies.put(source, x - 1);
- }
- }
- }
-
- private record DepOpen(FileRandomAccessBuffer raf, boolean fail) {}
-
- private DepOpen openDependency(File data, ShortBuffer buf, PeerNode source) {
- try {
- if (data != null) return new DepOpen(new FileRandomAccessBuffer(data, true), false);
- if (LOG.isErrorEnabled()) {
- LOG.error("Dependency with hash {} not found!", HexUtil.bytesToHex(buf.getData()));
- }
- return new DepOpen(null, true);
- } catch (IOException e) {
- LOG.error(
- "Peer {} asked us for the dependency with hash {} jar, we have downloaded it but {} even"
- + " though we did have it when we checked!: {}",
- source,
- HexUtil.bytesToHex(buf.getData()),
- e instanceof FileNotFoundException ? "don't have the file" : "can't read the file",
- e,
- e);
- return new DepOpen(null, true);
- }
- }
-
- /**
- * @return False if we cannot accept any more transfers from this node. True to accept the
- * transfer.
- */
- private boolean incrementDependencies(PeerNode source) {
- synchronized (peersFetchingDependencies) {
- Integer x = peersFetchingDependencies.get(source);
- if (x == null) x = 0;
- x++;
- if (x > MAX_TRANSFERS_PER_PEER) {
- LOG.info("Too many dependency transfers for peer {} - rejecting", source);
- return false;
- } else peersFetchingDependencies.put(source, x);
- return true;
- }
- }
-
- boolean fetchingUOM() {
- return fetchingUOM;
+ public boolean isFetchingMain() {
+ return false;
}
/** Callback notified when a dependency fetch completes successfully. */
@@ -2570,20 +1474,10 @@ private PeerNode findPeerWithFallback() {
boolean tryEverything = false;
while (true) {
HashSet The hint is key-scoped: editions fetched from a stale or different key are ignored.
+ * The hint is scoped to the normalized update URI: editions fetched from a stale or different
+ * update scope are ignored.
*/
void recordSuccessfulCoreInfoFetch(FreenetURI fetchedUri, int fetchedEdition) {
if (fetchedEdition < 0 || fetchedUri == null) {
return;
}
synchronized (this) {
- String fetchedPublicKey = extractPublicKeyMaterial(fetchedUri);
- String currentPublicKey = extractPublicKeyMaterial(updateURI);
- if (!currentPublicKey.equals(fetchedPublicKey)) {
+ String fetchedScope = normalizeFetchedEditionScope(fetchedUri);
+ String currentScope = normalizeFetchedEditionScope(updateURI);
+ if (!currentScope.equals(fetchedScope)) {
if (LOG.isDebugEnabled()) {
LOG.debug(
- "Ignoring fetched edition {} for stale key {}; current key {}",
+ "Ignoring fetched edition {} for stale scope {}; current scope {}",
fetchedEdition,
- fetchedPublicKey,
- currentPublicKey);
+ fetchedScope,
+ currentScope);
}
return;
}
- if (!currentPublicKey.equals(lastKnownGoodFetchedEditionKey)) {
- lastKnownGoodFetchedEditionKey = currentPublicKey;
+ if (!currentScope.equals(lastKnownGoodFetchedEditionKey)) {
+ lastKnownGoodFetchedEditionKey = currentScope;
lastKnownGoodFetchedEdition = -1;
}
if (fetchedEdition > lastKnownGoodFetchedEdition) {
lastKnownGoodFetchedEdition = fetchedEdition;
if (LOG.isDebugEnabled()) {
LOG.debug(
- "Recorded last known good fetched edition {} for key {}",
+ "Recorded last known good fetched edition {} for scope {}",
lastKnownGoodFetchedEdition,
- currentPublicKey);
+ currentScope);
}
}
}
@@ -1185,7 +1186,8 @@ public String get() {
@Override
public void set(String val) {
- lastKnownGoodFetchedEditionKey = sanitizePublicKeyMaterial(val);
+ lastKnownGoodFetchedEditionKey = sanitizeFetchedEditionScope(val);
+ alignLastKnownGoodFetchedEditionToCurrentUpdateScope();
}
}
@@ -1394,40 +1396,97 @@ private static int sanitizeFetchedEdition(Integer edition) {
return Math.max(-1, edition);
}
- private static String sanitizePublicKeyMaterial(String value) {
+ private static String sanitizeFetchedEditionScope(String value) {
String trimmed = trimConfigValue(value);
if (trimmed == null || trimmed.isEmpty()) {
return "";
}
- return isBarePublicKey(trimmed) ? trimmed : "";
+ if (isBarePublicKey(trimmed)) {
+ return trimmed;
+ }
+ try {
+ FreenetURI parsed = new FreenetURI(trimmed);
+ if (!parsed.isUSK() || parsed.hasMetaStrings()) {
+ return "";
+ }
+ return normalizeFetchedEditionScope(parsed);
+ } catch (MalformedURLException _) {
+ return "";
+ }
}
- private synchronized void alignLastKnownGoodFetchedEditionToCurrentUpdateKey() {
- String currentUpdatePublicKey = extractPublicKeyMaterial(updateURI);
- if (!currentUpdatePublicKey.equals(lastKnownGoodFetchedEditionKey)) {
+ private synchronized void alignLastKnownGoodFetchedEditionToCurrentUpdateScope() {
+ String currentUpdateScope = normalizeFetchedEditionScope(updateURI);
+ if (alignLegacyBareFetchedEditionScope(currentUpdateScope)) {
+ return;
+ }
+ if (!currentUpdateScope.equals(lastKnownGoodFetchedEditionKey)) {
if (LOG.isDebugEnabled()) {
LOG.debug(
- "Resetting persisted fetched edition {} due to key mismatch: persisted={}, current={}",
+ "Resetting persisted fetched edition {} due to scope mismatch: persisted={},"
+ + " current={}",
lastKnownGoodFetchedEdition,
lastKnownGoodFetchedEditionKey,
- currentUpdatePublicKey);
+ currentUpdateScope);
}
- resetLastKnownGoodFetchedEditionLocked(currentUpdatePublicKey);
+ resetLastKnownGoodFetchedEditionLocked(currentUpdateScope);
return;
}
lastKnownGoodFetchedEdition = sanitizeFetchedEdition(lastKnownGoodFetchedEdition);
}
- private int computeCoreUpdaterSubscribeEditionSeedLocked(String currentUpdatePublicKey) {
- if (!currentUpdatePublicKey.equals(lastKnownGoodFetchedEditionKey)) {
+ private boolean alignLegacyBareFetchedEditionScope(String currentUpdateScope) {
+ if (!isBarePublicKey(lastKnownGoodFetchedEditionKey)) {
+ return false;
+ }
+ if (!extractPublicKeyMaterial(updateURI).equals(lastKnownGoodFetchedEditionKey)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "Resetting persisted fetched edition {} due to legacy key mismatch: persisted={},"
+ + " current={}",
+ lastKnownGoodFetchedEdition,
+ lastKnownGoodFetchedEditionKey,
+ currentUpdateScope);
+ }
+ resetLastKnownGoodFetchedEditionLocked(currentUpdateScope);
+ return true;
+ }
+ String legacyInfoScope =
+ normalizeFetchedEditionScope(updateURI.setDocName(UPDATE_URI_DOC_NAME));
+ if (!legacyInfoScope.equals(currentUpdateScope)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "Resetting persisted fetched edition {} while migrating legacy key {} to custom"
+ + " scope {}",
+ lastKnownGoodFetchedEdition,
+ lastKnownGoodFetchedEditionKey,
+ currentUpdateScope);
+ }
+ resetLastKnownGoodFetchedEditionLocked(currentUpdateScope);
+ return true;
+ }
+ lastKnownGoodFetchedEditionKey = currentUpdateScope;
+ return false;
+ }
+
+ private int computeCoreUpdaterSubscribeEditionSeedLocked(String currentUpdateScope) {
+ if (!currentUpdateScope.equals(lastKnownGoodFetchedEditionKey)) {
return Version.currentBuildNumber();
}
return Math.max(Version.currentBuildNumber(), lastKnownGoodFetchedEdition);
}
- private void resetLastKnownGoodFetchedEditionLocked(String currentUpdatePublicKey) {
+ private void resetLastKnownGoodFetchedEditionLocked(String currentUpdateScope) {
lastKnownGoodFetchedEdition = -1;
- lastKnownGoodFetchedEditionKey = currentUpdatePublicKey;
+ lastKnownGoodFetchedEditionKey = currentUpdateScope;
+ }
+
+ private static String normalizeFetchedEditionScope(FreenetURI uri) {
+ FreenetURI normalized = uri;
+ if (normalized.isSSK() && normalized.isSSKForUSK()) {
+ normalized = normalized.uskForSSK();
+ }
+ return normalized.setSuggestedEdition(0).toString(false, false);
}
private static boolean isBarePublicKey(String value) {
@@ -1586,11 +1645,11 @@ public ByteCounter getByteCounter() {
public synchronized void startCoreUpdater() {
if (coreUpdater != null) return;
int subscribeEditionSeed =
- computeCoreUpdaterSubscribeEditionSeedLocked(extractPublicKeyMaterial(updateURI));
+ computeCoreUpdaterSubscribeEditionSeedLocked(normalizeFetchedEditionScope(updateURI));
NodeUpdaterParams params =
new NodeUpdaterParams(
this,
- getCoreInfoURI(),
+ getURI(),
Version.currentBuildNumber(),
-1,
Integer.MAX_VALUE,
diff --git a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
index b4cbde4d5e1..9841be84521 100644
--- a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
+++ b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
@@ -68,6 +68,8 @@ class NodeUpdateManagerTest {
private NodeUpdateManager manager;
private Config config;
+ private static final String DEFAULT_FETCHED_SCOPE =
+ "USK@" + NodeUpdateManager.UPDATE_URI + "/info/0";
@BeforeEach
void setUp() throws Exception {
@@ -316,6 +318,10 @@ void constructor_whenCustomUpdateUriPersisted_expectCustomValuePreserved() throw
// Assert
assertEquals(customUri, custom.getURI().toString(false, false));
assertEquals(customUri, persistedConfig.get("node.updater").getString("URI"));
+
+ // The core updater should subscribe to the configured custom docname channel.
+ custom.startCoreUpdater();
+ assertEquals("custom-update-doc", custom.getCoreUpdater().getUpdateKey().getDocName());
}
@Test
@@ -375,8 +381,31 @@ void constructor_whenPersistedEditionKeyMismatched_expectEditionResetAndKeyCanon
// Assert
assertEquals(-1, persistedConfig.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
assertEquals(
- NodeUpdateManager.UPDATE_URI,
+ DEFAULT_FETCHED_SCOPE,
+ persistedConfig.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ }
+
+ @Test
+ void constructor_whenLegacyBareHintAndCustomUpdateScope_expectEditionResetAndSeedAtCurrent()
+ throws Exception {
+ // Arrange
+ int seededEdition = Version.currentBuildNumber() + 12;
+ String customDoc = "custom-update-doc";
+ PersistentConfig persistedConfig =
+ createLegacyBareHintPersistedConfigForCustomScope(seededEdition, customDoc);
+
+ // Act
+ NodeUpdateManager migrated = new NodeUpdateManager(node, persistedConfig);
+ migrated.startCoreUpdater();
+
+ // Assert
+ assertEquals(-1, persistedConfig.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
+ assertEquals(
+ "USK@" + NodeUpdateManager.UPDATE_URI + "/" + customDoc + "/0",
persistedConfig.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ assertEquals(
+ Version.currentBuildNumber(),
+ migrated.getCoreUpdater().getUpdateKey().getSuggestedEdition());
}
@Test
@@ -395,7 +424,34 @@ void setURI_whenPublicKeyChanges_expectPersistedFetchedEditionReset() throws Exc
// Assert
assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
assertEquals(
- alternateKey, config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ "USK@" + alternateKey + "/info/0",
+ config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ }
+
+ @Test
+ void setURI_whenDocNameChangesOnSameKey_expectPersistedFetchedEditionReset() throws Exception {
+ // Arrange
+ int knownEdition = Version.currentBuildNumber() + 5;
+ manager.recordSuccessfulCoreInfoFetch(
+ manager.getCoreInfoURI().setSuggestedEdition(knownEdition), knownEdition);
+ String customDoc = "custom-info";
+ FreenetURI changedDocUri =
+ new FreenetURI(
+ "USK@"
+ + NodeUpdateManager.UPDATE_URI
+ + "/"
+ + customDoc
+ + "/"
+ + Version.currentBuildNumber());
+
+ // Act
+ manager.setURI(changedDocUri);
+
+ // Assert
+ assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
+ assertEquals(
+ "USK@" + NodeUpdateManager.UPDATE_URI + "/" + customDoc + "/0",
+ config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
}
@Test
@@ -410,7 +466,24 @@ void recordSuccessfulCoreInfoFetch_whenMatchingKey_expectPersistedHintUpdated()
// Assert
assertEquals(knownEdition, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
assertEquals(
- NodeUpdateManager.UPDATE_URI,
+ DEFAULT_FETCHED_SCOPE,
+ config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ }
+
+ @Test
+ void recordSuccessfulCoreInfoFetch_whenMatchingSskForUsk_expectPersistedHintUpdated() {
+ // Arrange
+ int knownEdition = Version.currentBuildNumber() + 6;
+ FreenetURI fetchedSskUri =
+ manager.getCoreInfoURI().setSuggestedEdition(knownEdition).sskForUSK();
+
+ // Act
+ manager.recordSuccessfulCoreInfoFetch(fetchedSskUri, knownEdition);
+
+ // Assert
+ assertEquals(knownEdition, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
+ assertEquals(
+ DEFAULT_FETCHED_SCOPE,
config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
}
@@ -428,7 +501,35 @@ void recordSuccessfulCoreInfoFetch_whenDifferentKey_expectPersistedHintUnchanged
// Assert
assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
assertEquals(
- NodeUpdateManager.UPDATE_URI,
+ DEFAULT_FETCHED_SCOPE,
+ config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
+ }
+
+ @Test
+ void recordSuccessfulCoreInfoFetch_whenSameKeyDifferentDoc_expectPersistedHintUnchanged()
+ throws Exception {
+ // Arrange
+ String customDoc = "custom-info";
+ FreenetURI changedDocUri =
+ new FreenetURI(
+ "USK@"
+ + NodeUpdateManager.UPDATE_URI
+ + "/"
+ + customDoc
+ + "/"
+ + Version.currentBuildNumber());
+ manager.setURI(changedDocUri);
+ int fetchedEdition = Version.currentBuildNumber() + 4;
+ FreenetURI staleDocUri =
+ new FreenetURI("USK@" + NodeUpdateManager.UPDATE_URI + "/info/" + fetchedEdition);
+
+ // Act
+ manager.recordSuccessfulCoreInfoFetch(staleDocUri, fetchedEdition);
+
+ // Assert
+ assertEquals(-1, config.get("node.updater").getInt("lastKnownGoodFetchedEdition"));
+ assertEquals(
+ "USK@" + NodeUpdateManager.UPDATE_URI + "/" + customDoc + "/0",
config.get("node.updater").getString("lastKnownGoodFetchedEditionKey"));
}
@@ -495,5 +596,20 @@ void maybeSendUOMAnnounce_whenNothingToAnnounce_expectNoSend() {
verifyNoInteractions(peer);
}
- // --- helpers (none) ---
+ private static PersistentConfig createLegacyBareHintPersistedConfigForCustomScope(
+ int seededEdition, String customDoc) {
+ String customUri =
+ "USK@"
+ + NodeUpdateManager.UPDATE_URI
+ + "/"
+ + customDoc
+ + "/"
+ + Version.currentBuildNumber();
+ SimpleFieldSet persisted = new SimpleFieldSet(true);
+ persisted.putSingle("node.updater.URI", customUri);
+ persisted.put("node.updater.lastKnownGoodFetchedEdition", seededEdition);
+ persisted.putSingle(
+ "node.updater.lastKnownGoodFetchedEditionKey", NodeUpdateManager.UPDATE_URI);
+ return new PersistentConfig(persisted);
+ }
}
From e51dad53ffeaef6afe5b0729040d3f0805ea9d93 Mon Sep 17 00:00:00 2001
From: Leumor <116955025+leumor@users.noreply.github.com>
Date: Sun, 1 Mar 2026 20:29:52 +0000
Subject: [PATCH 7/7] fix(updater): seed subscriptions below known-good
When a matching fetched-edition hint exists, subscribe one edition below the highest known fetched value to ensure USKManager emits immediate onFoundEdition callbacks after restart.
Keep current-build seeding for non-matching or near-current hints, and extend updater tests for higher-than-current and one-above-current seed behavior.
---
.../node/updater/NodeUpdateManager.java | 6 ++++-
.../node/updater/NodeUpdateManagerTest.java | 22 ++++++++++++++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java
index 9c8705e9ab5..a1d47ae6d2e 100644
--- a/src/main/java/network/crypta/node/updater/NodeUpdateManager.java
+++ b/src/main/java/network/crypta/node/updater/NodeUpdateManager.java
@@ -1473,7 +1473,11 @@ private int computeCoreUpdaterSubscribeEditionSeedLocked(String currentUpdateSco
if (!currentUpdateScope.equals(lastKnownGoodFetchedEditionKey)) {
return Version.currentBuildNumber();
}
- return Math.max(Version.currentBuildNumber(), lastKnownGoodFetchedEdition);
+ int highestKnownEdition = sanitizeFetchedEdition(lastKnownGoodFetchedEdition);
+ if (highestKnownEdition > Version.currentBuildNumber()) {
+ return highestKnownEdition - 1;
+ }
+ return Version.currentBuildNumber();
}
private void resetLastKnownGoodFetchedEditionLocked(String currentUpdateScope) {
diff --git a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
index 9841be84521..bae8b8ff647 100644
--- a/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
+++ b/src/test/java/network/crypta/node/updater/NodeUpdateManagerTest.java
@@ -341,7 +341,7 @@ void constructor_whenCustomUpdateUriPersisted_expectCustomValuePreserved() throw
seeded.startCoreUpdater();
// Assert
- assertEquals(seededEdition, seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition());
+ assertEquals(seededEdition - 1, seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition());
}
@Test
@@ -364,6 +364,26 @@ void startCoreUpdater_whenMatchingPersistedEditionLowerThanCurrent_expectSubscri
Version.currentBuildNumber(), seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition());
}
+ @Test
+ void startCoreUpdater_whenMatchingPersistedEditionOneAboveCurrent_expectSubscribeSeedAtCurrent()
+ throws Exception {
+ // Arrange
+ int seededEdition = Version.currentBuildNumber() + 1;
+ SimpleFieldSet persisted = new SimpleFieldSet(true);
+ persisted.put("node.updater.lastKnownGoodFetchedEdition", seededEdition);
+ persisted.putSingle(
+ "node.updater.lastKnownGoodFetchedEditionKey", NodeUpdateManager.UPDATE_URI);
+ PersistentConfig persistedConfig = new PersistentConfig(persisted);
+ NodeUpdateManager seeded = new NodeUpdateManager(node, persistedConfig);
+
+ // Act
+ seeded.startCoreUpdater();
+
+ // Assert
+ assertEquals(
+ Version.currentBuildNumber(), seeded.getCoreUpdater().getUpdateKey().getSuggestedEdition());
+ }
+
@Test
void constructor_whenPersistedEditionKeyMismatched_expectEditionResetAndKeyCanonicalized()
throws Exception {