diff --git a/.agents/skills/cryptad-architecture/SKILL.md b/.agents/skills/cryptad-architecture/SKILL.md index f727b2d620f..eaffa1c2626 100644 --- a/.agents/skills/cryptad-architecture/SKILL.md +++ b/.agents/skills/cryptad-architecture/SKILL.md @@ -19,9 +19,15 @@ Use this skill when you need to: - The root project still owns `buildJar`, `run`, `runLauncher`, distribution/jpackage tasks, the strongly coupled core packages, and all tests. - Leaf subprojects: - - `:foundation-config` → `network.crypta.config`, `network.crypta.l10n`, the main l10n - resources, and the current minimal config/l10n helper closure under - `network.crypta.support*` plus `network.crypta.node.FSParseException` + - `:foundation-support` → the current stable generic subset of `network.crypta.support`, + `network.crypta.support.api`, `network.crypta.support.io`, + `network.crypta.support.compress`, `network.crypta.support.math`, plus + `network.crypta.node.FSParseException` + - `:foundation-store-contracts` → neutral `network.crypta.store` contracts + `BlockMetadata`, `GetPubkey`, `StorableBlock` + - `:foundation-config` → `network.crypta.config`, `network.crypta.l10n`, and the main l10n + resources; public config APIs re-export `:foundation-support` and `:foundation-fs` where they + expose shared types - `:foundation-fs` → `network.crypta.fs` - `:foundation-compat` → `network.crypta.compat` - `:runtime-spi` → `network.crypta.runtime.spi` (JDK-only runtime/config boundary) @@ -43,8 +49,8 @@ Use this skill when you need to: - The large cyclic daemon core still lives in the root project: `network.crypta.node` (except the transitional `FSParseException` move), `network.crypta.io`, `network.crypta.client`, `network.crypta.clients`, - most of `network.crypta.support`, `network.crypta.crypt`, `network.crypta.keys`, - `network.crypta.store`, and `network.crypta.tools`. + the remaining daemon-coupled support code, `network.crypta.crypt`, `network.crypta.keys`, + most store implementations under `network.crypta.store`, and `network.crypta.tools`. - `:foundation-config` is the current home for all main `network.crypta.config` and `network.crypta.l10n` sources. Their unit tests still live in the root test tree and are run by the root project. @@ -53,9 +59,8 @@ Use this skill when you need to: package/resource moves, because stale non-owner aggregated outputs from earlier builds or branch switches can still shadow leaf outputs while `buildJar` packages aggregated main outputs first. - Update that metadata whenever a leaf starts owning additional main classes/resources that root - used to compile/package. If the split under `network.crypta.support*` keeps growing, prefer a - future extraction such as `:foundation-support`, but keep the metadata accurate until the build - no longer has that stale non-owner aggregated output shadowing risk. + used to compile/package. This applies to existing leaves such as `:foundation-support` and + `:foundation-store-contracts` just as much as any future extraction. ## Architecture overview (by package) ### Core network layer (`network.crypta.node`) @@ -69,6 +74,8 @@ Use this skill when you need to: - Storage abstractions: `FreenetStore` - CHK/SSK stores: `CHKStore`, `SSKStore` - Caching: `SlashdotStore` +- Neutral contracts `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in + `:foundation-store-contracts`; store implementations still remain in the root project. ### Cryptography (`network.crypta.crypt`) - Encryption: block cipher / AES streams @@ -185,10 +192,11 @@ Use this skill when you need to: ### Supporting infrastructure (`network.crypta.support`) - Logging, data structures, threading, helpers -- Most support code still lives in the root project, but `:foundation-config` now carries the - current minimal config/l10n closure from `network.crypta.support`, - `network.crypta.support.api`, and `network.crypta.support.io` so that config/l10n can compile as - a leaf without depending back on root main sources. +- `:foundation-support` now owns the stable generic support subset across `network.crypta.support`, + `network.crypta.support.api`, `network.crypta.support.io`, + `network.crypta.support.compress`, and `network.crypta.support.math`. +- The root project still owns daemon-coupled support code and higher-level wiring that is not yet + stable enough to extract cleanly. ### Launcher/Desktop leaf module (`:launcher-desktop`) - Swing launcher: `network.crypta.launcher` @@ -196,8 +204,10 @@ Use this skill when you need to: - Vendored OSHI annotations and launcher resources ### Foundation leaf modules -- `:foundation-config`: `network.crypta.config`, `network.crypta.l10n`, selected - `network.crypta.support*` helpers, and `network.crypta.node.FSParseException` +- `:foundation-support`: stable generic `network.crypta.support*` subset plus + `network.crypta.node.FSParseException` +- `:foundation-store-contracts`: neutral `network.crypta.store` contracts +- `:foundation-config`: `network.crypta.config`, `network.crypta.l10n` - `:foundation-fs`: `network.crypta.fs` - `:foundation-compat`: `network.crypta.compat` - `:runtime-spi`: `network.crypta.runtime.spi` diff --git a/.agents/skills/cryptad-build-test/SKILL.md b/.agents/skills/cryptad-build-test/SKILL.md index 28cd523cfaa..b7bf06739b5 100644 --- a/.agents/skills/cryptad-build-test/SKILL.md +++ b/.agents/skills/cryptad-build-test/SKILL.md @@ -18,12 +18,17 @@ Use this skill when you need to: ## Build layout - Cryptad now uses a partial multi-project Gradle build. - Use root-project tasks by default; the root project remains the daemon/application target. -- Current leaf projects are `:foundation-config`, `:foundation-fs`, `:foundation-compat`, - `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. +- Current leaf projects are `:foundation-support`, `:foundation-store-contracts`, + `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, + `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - The extracted leaf projects compile separately, but `buildJar`, `run`, `runLauncher`, `assembleCryptadDist`, and jpackage tasks are still rooted at `:cryptad`. -- `:foundation-config` owns the main `network.crypta.config` and `network.crypta.l10n` sources, - plus the minimal support/node compile closure they currently need. +- `:foundation-support` owns the current stable generic support subset under + `network.crypta.support*` plus `network.crypta.node.FSParseException`. +- `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts + `BlockMetadata`, `GetPubkey`, and `StorableBlock`. +- `:foundation-config` owns the main `network.crypta.config` and `network.crypta.l10n` sources. + Its public APIs now re-export `:foundation-support` and `:foundation-fs` where needed. - Every extracted internal leaf must keep leaf-owned aggregated-output metadata in sync at `/gradle/owned-output-patterns.txt`, even for structurally separate package/resource moves. Non-clean builds and branch switches can leave stale non-owner aggregated outputs behind, @@ -65,7 +70,12 @@ When running ./gradlew test via OpenCode bash, set timeout ≥ 15 minutes (≥ 9 ## Compile-only / quick checks - Compile only: - `./gradlew compileJava` -- Compile the config/l10n leaf when you touched extracted config, l10n, or their helper closure: +- Compile the support leaf when you touched extracted generic support classes: + - `./gradlew :foundation-support:classes` +- Compile the neutral store-contracts leaf when you touched `BlockMetadata`, `GetPubkey`, or + `StorableBlock`: + - `./gradlew :foundation-store-contracts:compileJava` +- Compile the config/l10n leaf when you touched extracted config or l10n sources: - `./gradlew :foundation-config:classes` - Compile only the runtime SPI leaf when you touched just that JDK-only API surface: - `./gradlew :runtime-spi:compileJava` diff --git a/.agents/skills/cryptad-packaging/SKILL.md b/.agents/skills/cryptad-packaging/SKILL.md index 5dfbeb37188..a65bacba908 100644 --- a/.agents/skills/cryptad-packaging/SKILL.md +++ b/.agents/skills/cryptad-packaging/SKILL.md @@ -20,12 +20,14 @@ Use this skill when working on: - Packaging remains root-owned. - The root project `:cryptad` still owns `buildJar`, `assembleCryptadDist`, `dist*`, `run`, `runLauncher`, and jpackage tasks. -- Current contributing leaf modules are `:foundation-config`, `:foundation-fs`, - `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and - `:launcher-desktop`. +- Current contributing leaf modules are `:foundation-support`, `:foundation-store-contracts`, + `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, + `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Extracted leaf modules contribute jars and resources through the root runtime classpath. +- `:foundation-support` and `:foundation-store-contracts` contribute shared runtime classes via + their leaf JARs like the other extracted modules. - `:foundation-config` contributes the config/l10n code and main l10n resources via its leaf JAR - like the other extracted modules. + and re-exports `:foundation-support` and `:foundation-fs` where public APIs expose those types. - The `:runtime-spi` JAR is packaged like the other leaf artifacts; packaging still produces one daemon distribution rooted at `:cryptad`. - Packaging does not have separate entrypoints per leaf project; it still assembles a single daemon diff --git a/README.md b/README.md index cf674c66df4..2c3b4f3ab82 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,16 @@ Cryptad now uses a partial multi-project Gradle build. - The root project remains the daemon/application project. It still owns the daemon JAR, tests, `run`, `runLauncher`, `assembleCryptadDist`, and jpackage task graph. -- `:foundation-config` owns `network.crypta.config`, `network.crypta.l10n`, the main - `network/crypta/l10n/crypta.l10n.en.properties` resource, and the current minimal helper - closure those packages need from `network.crypta.support*` plus +- `:foundation-support` owns the current stable generic support subset under + `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, + `network.crypta.support.compress`, `network.crypta.support.math`, plus `network.crypta.node.FSParseException`. +- `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts + `BlockMetadata`, `GetPubkey`, and `StorableBlock`. +- `:foundation-config` owns `network.crypta.config`, `network.crypta.l10n`, and the main + `network/crypta/l10n/crypta.l10n.en.properties` resource. Its public APIs now export + `:foundation-support` and `:foundation-fs` where config types expose `SimpleFieldSet` or + filesystem-facing value types. - `:foundation-fs` owns `network.crypta.fs`. - `:foundation-compat` owns `network.crypta.compat`. - `:runtime-spi` owns `network.crypta.runtime.spi` and the JDK-only runtime/config boundary used @@ -182,7 +188,7 @@ Cryptad now uses a partial multi-project Gradle build. - `:launcher-desktop` owns `network.crypta.launcher`, `com.jthemedetecor`, `oshi`, and launcher resources. - The large cyclic daemon core remains in the root project for now, and all tests still live - there. + there. Most store implementations and daemon-coupled support code remain root-owned. - Higher-level infrastructure now crosses a narrower boundary through `network.crypta.runtime.spi.RuntimePorts`, implemented in the root project by `network.crypta.node.runtime.LegacyRuntimePorts`, plus HTTP-local wiring records and @@ -462,8 +468,12 @@ cd build/jpackage/Crypta.app/Contents artifacts; use the commands in “Spotless + Dependency Verification” below. Root build also includes: -- `:foundation-config`: extracted config/l10n code plus the minimal support/node closure needed - to compile it as a leaf. +- `:foundation-support`: extracted stable support/api/io/compress/math subset plus + `network.crypta.node.FSParseException`. +- `:foundation-store-contracts`: neutral store contracts used by `crypt`, `keys`, and `store` + code during extraction work. +- `:foundation-config`: extracted config/l10n code and main l10n resources. Its public APIs + re-export `:foundation-support` and `:foundation-fs` where required. - `:launcher-desktop`: Swing launcher code and desktop/theme detection dependencies. - `:thirdparty-onion`: Onion FEC and related vendored sources/resources. - `:thirdparty-legacy`: Bitpedia, SevenZip, and Spaceroots vendored code. @@ -516,10 +526,13 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF - Root project `:cryptad` remains the daemon/application build and still owns the strongly coupled core packages, all tests, packaging/runtime tasks, and the current `LegacyRuntimePorts` bridge into the runtime SPI. - - Leaf subprojects are `:foundation-config`, `:foundation-fs`, `:foundation-compat`, - `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. + - Leaf subprojects are `:foundation-support`, `:foundation-store-contracts`, + `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, + `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Core network (`network.crypta.node`): `Node`, `PeerNode`, `PeerManager`, `PacketSender`, `RequestStarter`, `RequestScheduler`, `NodeUpdateManager`. - Storage (`network.crypta.store`): `FreenetStore`, `CHKStore`, `SSKStore`, `SlashdotStore`. + Neutral contracts such as `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in + `:foundation-store-contracts`; store implementations still remain in the root project. - Crypto (`network.crypta.crypt`): AES, DSA/ECDSA, SHA‑256, `RandomSource`/Yarrow. - Keys (`network.crypta.keys`): `ClientCHK`, `ClientSSK`, `FreenetURI`, USK. - Clients: `network.crypta.client`, FCP (`network.crypta.clients.fcp`), HTTP @@ -549,19 +562,22 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF `SecurityLevelsSnapshot`, `PageChromeSnapshot`, `FirstTimeWizardSnapshot`, `FirstTimeWizardCurrentBandwidthLimits`, `ToadletSymlinkEntry`, and `WelcomePageSnapshot`. - Config + localization leaf (`:foundation-config`): `network.crypta.config`, - `network.crypta.l10n`, the main l10n properties, and the current minimal compile closure they - need from `network.crypta.support`, `network.crypta.support.api`, - `network.crypta.support.io`, and `network.crypta.node.FSParseException`. Higher layers should - still prefer `RuntimePorts#config()` and the root `LegacyConfigPort` bridge instead of reaching - through daemon internals. -- Support (`network.crypta.support`): logging, data structures, threading, and helpers remain - mostly in the root project; the config/l10n-specific compile closure is temporarily housed in - `:foundation-config` while package names stay unchanged. + `network.crypta.l10n`, and the main l10n properties. Its public APIs re-export + `:foundation-support` and `:foundation-fs` where config surfaces expose `SimpleFieldSet` or + filesystem-facing types. Higher layers should still prefer `RuntimePorts#config()` and the root + `LegacyConfigPort` bridge instead of reaching through daemon internals. +- Support foundation leaf (`:foundation-support`): stable generic support, support-api, + support-io, support-compress, and support-math classes plus + `network.crypta.node.FSParseException`. +- Support (`network.crypta.support`): logging, data structures, threading, and helpers are now + split between `:foundation-support` and the root project. Keep generic reusable utilities in the + foundation leaf; daemon-coupled support code still remains in root. - Launcher/Desktop: `:launcher-desktop` provides `network.crypta.launcher`, `com.jthemedetecor`, launcher resources, and desktop-theme integration. -- Extracted foundations: `:foundation-config` provides config/l10n plus the minimal support/node - closure described above, `:foundation-fs` provides `network.crypta.fs`, - and `:foundation-compat` provides `network.crypta.compat`. +- Extracted foundations: `:foundation-support` provides the stable generic support subset, + `:foundation-store-contracts` provides neutral `network.crypta.store` contracts, + `:foundation-config` provides config/l10n, `:foundation-fs` provides `network.crypta.fs`, and + `:foundation-compat` provides `network.crypta.compat`. - Runtime boundary leaf: `:runtime-spi` provides `network.crypta.runtime.spi`. - Vendored libraries: `:thirdparty-onion` provides `com.onionnetworks`, `:thirdparty-legacy` provides `org.bitpedia`, `org.sevenzip`, and `org.spaceroots`. diff --git a/build.gradle.kts b/build.gradle.kts index 843640fbd7b..98ab1e9fe5c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,3 @@ -import java.io.File - plugins { // Apply Gradle 9 convention plugins from the included build id("cryptad.java-kotlin-conventions") @@ -19,6 +17,7 @@ version = "3" val internalLeafProjects = listOf( project(":foundation-support"), + project(":foundation-store-contracts"), project(":foundation-config"), project(":foundation-fs"), project(":foundation-compat"), @@ -37,6 +36,7 @@ val internalLeafMainClassDirs = dependencies { // implementation implementation(project(":foundation-support")) + implementation(project(":foundation-store-contracts")) implementation(project(":foundation-config")) implementation(project(":foundation-fs")) implementation(project(":foundation-compat")) @@ -145,10 +145,12 @@ val aggregatedMainOutputProducers = (listOf(project) + internalLeafProjects).map(::AggregatedMainOutputProducer) // Every extracted internal leaf declares aggregated main-output ownership metadata under -// /gradle/owned-output-patterns.txt. Structurally separated root -> leaf and leaf -> leaf -// moves need this because stale outputs can survive in root or old leaf build directories on -// non-clean builds or branch switches, while root packaging/runtime aggregation still consumes the -// root main output and every internal leaf main output. +// /gradle/owned-output-patterns.txt. +// We need this for structurally separated root -> leaf and leaf -> leaf moves. +// Stale outputs can survive in root or old leaf build directories on non-clean builds or branch +// switches. +// Root packaging/runtime aggregation still consumes the root main output and every internal leaf +// main output. val selectiveLeafOutputOwnerships = internalLeafProjects.map { leaf -> val metadataFile = @@ -247,6 +249,7 @@ val selectiveLeafOutputPruneTasks = aggregatedMainOutputProducers .filter { producer -> producer.project != ownership.leaf } .flatMap { producer -> ownedOutputTreesFor(producer, ownership.patterns) } + group = "build" description = "Removes stale non-owner aggregated main outputs for paths owned by ${ownership.leaf.path} on non-clean builds" dependsOn(verifySelectiveLeafOwnershipMetadata) @@ -257,6 +260,7 @@ val selectiveLeafOutputPruneTasks = val pruneSelectiveLeafOutputs by tasks.registering { + group = "build" description = "Removes stale aggregated main outputs claimed by selectively extracted leaf projects on non-clean builds" dependsOn(verifySelectiveLeafOwnershipMetadata) @@ -305,12 +309,7 @@ tasks.named("buildJar") { dependsOn(pruneSelectiveLeafOutputs) dependsOn(internalLeafProjects.map { "${it.path}:classes" }) internalLeafProjects.forEach { leaf -> - from( - leaf.extensions - .getByType(org.gradle.api.tasks.SourceSetContainer::class.java) - .named("main") - .map { it.output } - ) + from(leaf.extensions.getByType(SourceSetContainer::class.java).named("main").map { it.output }) } } diff --git a/foundation-store-contracts/build.gradle.kts b/foundation-store-contracts/build.gradle.kts new file mode 100644 index 00000000000..442c408a7a0 --- /dev/null +++ b/foundation-store-contracts/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + id("cryptad.java-kotlin-conventions") + id("cryptad.spotless") +} + +version = rootProject.version diff --git a/foundation-store-contracts/gradle/owned-output-patterns.txt b/foundation-store-contracts/gradle/owned-output-patterns.txt new file mode 100644 index 00000000000..171714276bb --- /dev/null +++ b/foundation-store-contracts/gradle/owned-output-patterns.txt @@ -0,0 +1,6 @@ +# Aggregated main outputs that must be pruned on non-clean builds because :foundation-store-contracts owns them now. + +network/crypta/store/StorableBlock* +network/crypta/store/BlockMetadata* +network/crypta/store/GetPubkey* + diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/BlockMetadata.java b/foundation-store-contracts/src/main/java/network/crypta/store/BlockMetadata.java new file mode 100644 index 00000000000..793b0064a14 --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/BlockMetadata.java @@ -0,0 +1,35 @@ +package network.crypta.store; + +/** + * Mutable metadata describing properties of a stored block. + * + *

This type is a simple value holder. It performs no validation or synchronization. + */ +public final class BlockMetadata { + + /** + * Indicates that the block came from an older or opportunistic cache path. + * + *

Callers may use this to avoid treating the block as a normal promotable datastore entry. + */ + private boolean oldBlock; + + /** Clears all metadata and restores default values. */ + public void reset() { + oldBlock = false; + } + + /** + * Reports whether the block should be treated as old. + * + * @return {@code true} when the caller marked the block as old; otherwise {@code false} + */ + public boolean isOldBlock() { + return oldBlock; + } + + /** Marks the block as old. */ + public void setOldBlock() { + oldBlock = true; + } +} diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/GetPubkey.java b/foundation-store-contracts/src/main/java/network/crypta/store/GetPubkey.java new file mode 100644 index 00000000000..ad5e050f6ac --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/GetPubkey.java @@ -0,0 +1,44 @@ +package network.crypta.store; + +/** + * Lookup and caching facade for public keys or similar verification material. + * + *

Implementations resolve key material by hash and may cache or promote the result across + * multiple storage layers. Methods do not declare checked exceptions; implementations typically + * return {@code null} on lookup failure and suppress write failures after logging. + * + * @param

key material type returned and cached by the implementation + */ +public interface GetPubkey

{ + + /** + * Returns key material for a given hash. + * + * @param hash key hash + * @param canReadClientCache whether the client cache may be consulted + * @param forULPR hint allowing the implementation to use an alternate cache path + * @param meta optional metadata sink that may be updated during lookup; may be {@code null} + * @return resolved key material, or {@code null} if not found + */ + P getKey(byte[] hash, boolean canReadClientCache, boolean forULPR, BlockMetadata meta); + + /** + * Caches key material and, when permitted, promotes it to deeper storage. + * + * @param hash key hash + * @param key key material to cache + * @param deep whether to promote it to deep storage rather than only cache layers + * @param canWriteClientCache whether the client cache may be written + * @param canWriteDatastore whether normal datastore writes are allowed + * @param forULPR hint allowing writes to an alternate cache path + * @param writeLocalToDatastore whether local writes may bypass normal datastore restrictions + */ + void cacheKey( + byte[] hash, + P key, + boolean deep, + boolean canWriteClientCache, + boolean canWriteDatastore, + boolean forULPR, + boolean writeLocalToDatastore); +} diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/StorableBlock.java b/foundation-store-contracts/src/main/java/network/crypta/store/StorableBlock.java new file mode 100644 index 00000000000..896426b0eaa --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/StorableBlock.java @@ -0,0 +1,31 @@ +package network.crypta.store; + +/** + * Minimal contract for a block that can be addressed by store code. + * + *

Implementations expose two opaque identifiers: + * + *

    + *
  • A routing key used for lookups and indexing. + *
  • A full key that identifies the exact serialized block instance. + *
+ * + *

Neither identifier's encoding is prescribed here. Callers must treat returned arrays as + * read-only; implementations may return internal buffers. + */ +public interface StorableBlock { + + /** + * Returns the routing key used to locate this block. + * + * @return opaque routing-key bytes + */ + byte[] getRoutingKey(); + + /** + * Returns the full key that uniquely identifies this block instance. + * + * @return opaque full-key bytes + */ + byte[] getFullKey(); +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 98f7d4e33c8..03e243485d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ rootProject.name = "cryptad" include( ":foundation-support", + ":foundation-store-contracts", ":foundation-config", ":foundation-fs", ":foundation-compat", diff --git a/src/main/java/network/crypta/keys/NodeSSK.java b/src/main/java/network/crypta/keys/NodeSSK.java index b13f81469b4..5cf53b9b1ec 100644 --- a/src/main/java/network/crypta/keys/NodeSSK.java +++ b/src/main/java/network/crypta/keys/NodeSSK.java @@ -419,7 +419,10 @@ public static NodeSSK construct(byte[] buf) throws SSKVerifyException { * @return {@code true} if a key was fetched and attached; {@code false} otherwise. */ public boolean grabPubkey( - GetPubkey pubkeyCache, boolean canReadClientCache, boolean forULPR, BlockMetadata meta) { + GetPubkey pubkeyCache, + boolean canReadClientCache, + boolean forULPR, + BlockMetadata meta) { if (pubKey != null) return false; pubKey = pubkeyCache.getKey(pubKeyHash, canReadClientCache, forULPR, meta); return pubKey != null; @@ -494,7 +497,10 @@ public void setPubKey(DSAPublicKey pubKey2) { */ @Override public boolean grabPubkey( - GetPubkey pubkeyCache, boolean canReadClientCache, boolean forULPR, BlockMetadata meta) { + GetPubkey pubkeyCache, + boolean canReadClientCache, + boolean forULPR, + BlockMetadata meta) { throw new UnsupportedOperationException(); } } diff --git a/src/main/java/network/crypta/node/NodeGetPubkey.java b/src/main/java/network/crypta/node/NodeGetPubkey.java index 09225c1c51a..7b0391671be 100644 --- a/src/main/java/network/crypta/node/NodeGetPubkey.java +++ b/src/main/java/network/crypta/node/NodeGetPubkey.java @@ -22,7 +22,7 @@ *

Lookup order (when applicable): memory cache → client caches (including legacy) → main * datastore (including legacy) → datacache (including legacy) → slashdot cache (when requested). */ -public class NodeGetPubkey implements GetPubkey { +public class NodeGetPubkey implements GetPubkey { private static final Logger LOG = LoggerFactory.getLogger(NodeGetPubkey.class); // Enable the small RAM-backed pubkey cache for hot entries. @@ -96,7 +96,7 @@ public DSAPublicKey getKey( tryDataCaches(hash, ignoreOldBlocks, meta), () -> trySlashdot(hash, forULPR, ignoreOldBlocks, meta)))); if (key != null) { - // Populate the in-memory cache for subsequent lookups. + // Populate the in-memory cache for later lookups. cacheKey(hash, key, false, false, false, false, false); } return key; diff --git a/src/main/java/network/crypta/store/BlockMetadata.java b/src/main/java/network/crypta/store/BlockMetadata.java deleted file mode 100644 index a48bc3a5053..00000000000 --- a/src/main/java/network/crypta/store/BlockMetadata.java +++ /dev/null @@ -1,51 +0,0 @@ -package network.crypta.store; - -/** - * Mutable metadata describing properties of a datastore block. - * - *

Instances accompany blocks read from the store and influence higher-level caching and - * advertising decisions. This type is a simple value holder and performs no validation or - * synchronization; callers are responsible for any required thread-safety when sharing instances - * across threads. - */ -public final class BlockMetadata { - - /** - * Flag indicating that the block originates from legacy or opportunistic caching. - * - *

When {@code true}, the block was either persisted before build 1224 or cached because a low - * physical security level caused all traffic to be written to the datastore. In such cases we - * cannot assert that it should be cached or advertised to peers; other nodes should learn about - * it only when we actively transmit the data. - */ - private boolean oldBlock; - - /** - * Clear all metadata and restore default values. - * - *

Postcondition: {@link #isOldBlock()} returns {@code false}. - */ - public void reset() { - oldBlock = false; - } - - /** - * Report whether the block should be treated as "old" and therefore not proactively cached or - * advertised. - * - * @return {@code true} if the block predates build 1224 or was cached only due to global - * write-to-store settings (e.g., low physical security level); {@code false} otherwise. - */ - public boolean isOldBlock() { - return oldBlock; - } - - /** - * Mark the block as originating from legacy or opportunistic caching. - * - *

Postcondition: {@link #isOldBlock()} returns {@code true}. - */ - public void setOldBlock() { - oldBlock = true; - } -} diff --git a/src/main/java/network/crypta/store/GetPubkey.java b/src/main/java/network/crypta/store/GetPubkey.java deleted file mode 100644 index b83defa1512..00000000000 --- a/src/main/java/network/crypta/store/GetPubkey.java +++ /dev/null @@ -1,75 +0,0 @@ -package network.crypta.store; - -import network.crypta.crypt.DSAPublicKey; - -/** - * Public-key lookup and caching facade. - * - *

Implementations resolve DSA public keys by their hash and optionally cache or promote the - * result across several layers (client cache, main cache, persistent datastore, and an optional - * "slashdot" cache used for specific routing paths). Methods in this interface never declare - * checked exceptions; implementations typically return {@code null} on I/O errors or when a key is - * not present. - */ -public interface GetPubkey { - - /** - * Returns the public key for a given hash. - * - *

Implementations may consult multiple stores based on the provided flags. When {@code - * canReadClientCache} is {@code true}, the per-client cache may be queried. When {@code forULPR} - * is {@code true}, an implementation may also consult a specialized "slashdot" cache if - * available. The {@code meta} container may be populated with properties describing the origin of - * the block (for example, whether it should be treated as an "old" block for promotion logic). - * - * @param hash the hash of the public key (typically {@link DSAPublicKey#HASH_LENGTH} bytes as - * used by SSKs). - * @param canReadClientCache whether the local client cache may be consulted. - * @param forULPR hint allowing the implementation to consult the optional "slashdot" cache. - * @param meta optional metadata container that the implementation may update; may be {@code - * null}. - * @return the matching public key, or {@code null} if not found or on error. - */ - DSAPublicKey getKey(byte[] hash, boolean canReadClientCache, boolean forULPR, BlockMetadata meta); - - /** - * Caches a public key and, optionally, promotes it to the persistent datastore. - * - *

Writes are directed to one or more layers depending on the flags: - * - *

    - *
  • When {@code deep} is {@code true}, the key may be written to the persistent datastore; - * otherwise only to the main cache. - *
  • When {@code canWriteClientCache} is {@code true} and promotion to the datastore is - * disallowed, the key may be written to the per-client cache. - *
  • When {@code forULPR} is {@code true} and promotion is disallowed, the key may be written - * to a specialized "slashdot" cache if present. - *
  • When {@code canWriteDatastore} is {@code true}, normal promotion to the main cache and/or - * datastore is permitted. Implementations may mark entries as "old" when this flag is - * {@code false} to avoid proactive advertising. - *
  • When {@code writeLocalToDatastore} is {@code true}, an implementation may allow writing - * to the datastore even if {@code canWriteDatastore} is {@code false} (for example, for - * locally originated items). - *
- * - * @param hash hash of the public key being stored. - * @param key key material to store. - * @param deep whether to write to the persistent datastore ({@code true}) or only to the main - * cache ({@code false}). - * @param canWriteClientCache whether the per-client cache may be written (typically for local - * requests when enabled by configuration). - * @param canWriteDatastore whether promotion to the main cache/datastore is permitted. - * @param forULPR hint allowing writes to a specialized "slashdot" cache when promotion is - * disallowed. - * @param writeLocalToDatastore override permitting datastore writes even when {@code - * canWriteDatastore} is {@code false}. - */ - void cacheKey( - byte[] hash, - DSAPublicKey key, - boolean deep, - boolean canWriteClientCache, - boolean canWriteDatastore, - boolean forULPR, - boolean writeLocalToDatastore); -} diff --git a/src/main/java/network/crypta/store/SSKStore.java b/src/main/java/network/crypta/store/SSKStore.java index dd7afdb1f4a..dabdfcb4a51 100644 --- a/src/main/java/network/crypta/store/SSKStore.java +++ b/src/main/java/network/crypta/store/SSKStore.java @@ -18,7 +18,7 @@ public class SSKStore extends StoreCallback { /** Provider used to retrieve and cache DSA public keys by hash when reconstructing SSKs. */ - private final GetPubkey pubkeyCache; + private final GetPubkey pubkeyCache; /** * Creates a new store adapter for SSK blocks. @@ -26,7 +26,7 @@ public class SSKStore extends StoreCallback { * @param pubkeyCache source used to resolve {@link DSAPublicKey}s by their SHA‑256 hash when the * key is not already attached to the {@link NodeSSK} being reconstructed. */ - public SSKStore(GetPubkey pubkeyCache) { + public SSKStore(GetPubkey pubkeyCache) { this.pubkeyCache = pubkeyCache; } diff --git a/src/main/java/network/crypta/store/SimpleGetPubkey.java b/src/main/java/network/crypta/store/SimpleGetPubkey.java index 69b8a6afb63..4db5f9dc7d3 100644 --- a/src/main/java/network/crypta/store/SimpleGetPubkey.java +++ b/src/main/java/network/crypta/store/SimpleGetPubkey.java @@ -18,7 +18,7 @@ * mutable state. Concurrency characteristics therefore depend entirely on the {@link PubkeyStore} * implementation supplied at construction time. */ -public class SimpleGetPubkey implements GetPubkey { +public class SimpleGetPubkey implements GetPubkey { private static final Logger LOG = LoggerFactory.getLogger(SimpleGetPubkey.class); // Backing store used for all lookups and writes. diff --git a/src/main/java/network/crypta/store/StorableBlock.java b/src/main/java/network/crypta/store/StorableBlock.java deleted file mode 100644 index 69a01ec5250..00000000000 --- a/src/main/java/network/crypta/store/StorableBlock.java +++ /dev/null @@ -1,46 +0,0 @@ -package network.crypta.store; - -/** - * Minimal contract for a block that can be persisted in a {@link FreenetStore}. - * - *

Implementations expose two opaque identifiers: - * - *

    - *
  • Routing key — used as the datastore lookup key and for Bloom/probability checks. Its - * length is defined by the associated {@link StoreCallback#routingKeyLength()} and the format - * is implementation-specific. - *
  • Full key — a canonical identifier for the exact block instance. When the store is - * configured to keep full keys (see {@link StoreCallback#storeFullKeys()}), this value is - * written to the optional .keys side file to allow reconstruction and collision - * checks. - *
- * - *

Neither key's contents or encoding are prescribed by this interface. Callers must treat the - * returned arrays as read-only; implementations may return internal buffers for efficiency. - */ -public interface StorableBlock { - - /** - * Returns the routing key used to locate this block within a datastore. - * - *

The value is opaque to callers. Its size should match {@link - * StoreCallback#routingKeyLength()}. The returned array may be an internal buffer and must be - * treated as read-only; copy it if you need to retain or mutate it. - * - * @return byte array representing the routing key. - */ - byte[] getRoutingKey(); - - /** - * Returns the full key that uniquely identifies this block instance. - * - *

The full key can be stored alongside data for reconstruction and validation when enabled by - * {@link StoreCallback#storeFullKeys()}. Its size should match {@link - * StoreCallback#fullKeyLength()}. Implementations may return the same bytes as {@link - * #getRoutingKey()} when both identifiers are equivalent for the block type. The returned array - * must be treated as read-only. - * - * @return byte array representing the full key. - */ - byte[] getFullKey(); -} diff --git a/src/test/java/network/crypta/keys/NodeSSKTest.java b/src/test/java/network/crypta/keys/NodeSSKTest.java index d3fc5ec6dc2..42fe2d53777 100644 --- a/src/test/java/network/crypta/keys/NodeSSKTest.java +++ b/src/test/java/network/crypta/keys/NodeSSKTest.java @@ -202,7 +202,7 @@ void construct_whenUnknownAlgorithm_throwsSSKVerifyException() { byte[] pkh = bytes(NodeSSK.PUBKEY_HASH_SIZE, 0x11); NodeSSK key = new NodeSSK(pkh, eh, (byte) 99); byte[] full = key.getFullKey(); - // Make header look like SSK but with unsupported algo + // Make the header look like SSK but with unsupported algo full[0] = NodeSSK.BASE_TYPE; full[1] = Key.ALGO_AES_CTR_256_SHA256; // not accepted by construct() assertThrows(SSKVerifyException.class, () -> NodeSSK.construct(full)); @@ -222,7 +222,7 @@ void hasPubKey_getPubKey_and_setPubKey_happyPath() throws SSKVerifyException { assertTrue(key.hasPubKey()); assertEquals(pub, key.getPubKey()); - // Idempotent: setting same instance is a no-op + // Idempotent: setting the same instance is a no-op key.setPubKey(pub); assertEquals(pub, key.getPubKey()); } @@ -319,7 +319,7 @@ void compareTo_ordersByEncryptedDocnameThenPubkeyHash() { assertTrue(a.compareTo(b) < 0); assertTrue(b.compareTo(a) > 0); - // Now equal ehd; order by pubkey hash + // Now equal ehd; ordered by pubkey hash NodeSSK c = new NodeSSK(pkhA, ehA, Key.ALGO_AES_PCFB_256_SHA256); NodeSSK d = new NodeSSK(pkhB, ehA, Key.ALGO_AES_PCFB_256_SHA256); assertTrue(c.compareTo(d) < 0); @@ -332,7 +332,7 @@ void routingKeyFromFullKey_whenLengthMismatch_logsButComputesDeterministically() NodeSSK key = new NodeSSK(pkh, eh, Key.ALGO_AES_PCFB_256_SHA256); byte[] full = key.getFullKey(); - // Truncate to simulate wrong length; Arrays.copyOfRange pads with zeros + // Truncate to simulate the wrong length; Arrays.copyOfRange pads with zeros byte[] truncated = Arrays.copyOf(full, full.length - 3); byte[] rk = NodeSSK.routingKeyFromFullKey(truncated); @@ -362,21 +362,36 @@ void archivalCopy_returnsArchiveNodeSSK_andIsIndependent() { assertThrows(UnsupportedOperationException.class, () -> ((NodeSSK) archived).setPubKey(null)); } + @Test + void archivalCopy_whenGrabPubkeyCalled_throwsUnsupportedOperationException( + @Mock GetPubkey cache) { + byte[] eh = bytes(NodeSSK.E_H_DOCNAME_SIZE, 0x52); + byte[] pkh = bytes(NodeSSK.PUBKEY_HASH_SIZE, 0x53); + NodeSSK archived = (NodeSSK) new NodeSSK(pkh, eh, Key.ALGO_AES_PCFB_256_SHA256).archivalCopy(); + BlockMetadata meta = new BlockMetadata(); + + assertThrows( + UnsupportedOperationException.class, () -> archived.grabPubkey(cache, true, true, meta)); + } + @Test void grabPubkey_whenCacheHasKey_setsAndReturnsTrue( - @Mock GetPubkey cache, @Mock DSAPublicKey pub) { + @Mock GetPubkey cache, @Mock DSAPublicKey pub) { byte[] eh = bytes(NodeSSK.E_H_DOCNAME_SIZE, 0x71); byte[] pkh = bytes(NodeSSK.PUBKEY_HASH_SIZE, 0x72); NodeSSK key = new NodeSSK(pkh, eh, Key.ALGO_AES_PCFB_256_SHA256); + BlockMetadata meta = new BlockMetadata(); + + doReturn(pub).when(cache).getKey(pkh, true, false, meta); + boolean grabbed = key.grabPubkey(cache, true, false, meta); - doReturn(pub).when(cache).getKey(pkh, true, false, null); - boolean grabbed = key.grabPubkey(cache, true, false, null); assertTrue(grabbed); assertEquals(pub, key.getPubKey()); + verify(cache).getKey(pkh, true, false, meta); } @Test - void grabPubkey_whenCacheMiss_returnsFalseAndKeepsNull(@Mock GetPubkey cache) { + void grabPubkey_whenCacheMiss_returnsFalseAndKeepsNull(@Mock GetPubkey cache) { byte[] eh = bytes(NodeSSK.E_H_DOCNAME_SIZE, 0x74); byte[] pkh = bytes(NodeSSK.PUBKEY_HASH_SIZE, 0x75); NodeSSK key = new NodeSSK(pkh, eh, Key.ALGO_AES_PCFB_256_SHA256); @@ -388,8 +403,8 @@ void grabPubkey_whenCacheMiss_returnsFalseAndKeepsNull(@Mock GetPubkey cache) { } @Test - void grabPubkey_whenAlreadyHasKey_doesNotCallCache(@Mock GetPubkey cache, @Mock DSAPublicKey pub) - throws SSKVerifyException { + void grabPubkey_whenAlreadyHasKey_doesNotCallCache( + @Mock GetPubkey cache, @Mock DSAPublicKey pub) throws SSKVerifyException { byte[] eh = bytes(NodeSSK.E_H_DOCNAME_SIZE, 0x76); byte[] pubBytes = "grabbed".getBytes(StandardCharsets.US_ASCII); byte[] pkh = sha256(pubBytes); diff --git a/src/test/java/network/crypta/store/SSKStoreTest.java b/src/test/java/network/crypta/store/SSKStoreTest.java index 3cbb42d49d8..7a1d6ba98a3 100644 --- a/src/test/java/network/crypta/store/SSKStoreTest.java +++ b/src/test/java/network/crypta/store/SSKStoreTest.java @@ -43,7 +43,7 @@ class SSKStoreTest { private static final byte CRYPTO_ALGO = Key.ALGO_AES_PCFB_256_SHA256; - @Mock private GetPubkey pubkeyCache; + @Mock private GetPubkey pubkeyCache; @Mock private FreenetStore mockStore; diff --git a/src/test/java/network/crypta/store/SimplePubkeyCacheTest.java b/src/test/java/network/crypta/store/SimplePubkeyCacheTest.java index 967de57e87a..91bf5ebf14e 100644 --- a/src/test/java/network/crypta/store/SimplePubkeyCacheTest.java +++ b/src/test/java/network/crypta/store/SimplePubkeyCacheTest.java @@ -11,7 +11,7 @@ import network.crypta.support.math.MersenneTwister; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; class SimplePubkeyCacheTest { @@ -19,8 +19,9 @@ class SimplePubkeyCacheTest { void testSimple() { final int keys = 10; PubkeyStore pk = new PubkeyStore(); + //noinspection resource new RAMFreenetStore<>(pk, keys); - GetPubkey pubkeys = new SimpleGetPubkey(pk); + GetPubkey pubkeys = new SimpleGetPubkey(pk); DSAGroup group = Global.DSAgroupBigA; Random random = new MersenneTwister(1010101); HashMap map = new HashMap<>(); diff --git a/src/test/java/network/crypta/store/caching/CachingFreenetStoreTest.java b/src/test/java/network/crypta/store/caching/CachingFreenetStoreTest.java index f7326bfe83c..b34dad3dcd7 100644 --- a/src/test/java/network/crypta/store/caching/CachingFreenetStoreTest.java +++ b/src/test/java/network/crypta/store/caching/CachingFreenetStoreTest.java @@ -297,7 +297,7 @@ void put_whenSSKCollisionsAndExceedMax_expectEvictionAndCorrectWrites() // Arrange PubkeyStore pk = new PubkeyStore(); try (var _ = new RAMFreenetStore<>(pk, 10)) { - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); int sskBlockSize = store.getTotalBlockSize(); @@ -439,7 +439,7 @@ void pushLeastRecentlyBlock_whenSingleCached_expectWriteAndThenEmpty() // Arrange PubkeyStore pk = new PubkeyStore(); try (var _ = new RAMFreenetStore<>(pk, 10)) { - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); int sskBlockSize = store.getTotalBlockSize(); @@ -528,7 +528,7 @@ void pushLeastRecentlyBlock_whenConcurrentOverwrite_expectReturnZero() RAMFreenetStore ramFreenetStore = new RAMFreenetStore<>(pk, 10); pk.setStore(ramFreenetStore); - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); int sskBlockSize = store.getTotalBlockSize(); @@ -667,7 +667,7 @@ void putAndFetch_whenSSKInserted_expectCacheHitAndUnderlyingMiss() RAMFreenetStore ramFreenetStore = new RAMFreenetStore<>(pk, keys); pk.setStore(ramFreenetStore); - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); File f = getStorePath("testSimpleSSK"); try (SaltedHashFreenetStore saltStore = @@ -892,7 +892,7 @@ void close_whenReopenSSK_expectBlocksPersistedInUnderlying() PubkeyStore pk = new PubkeyStore(); RAMFreenetStore ramFreenetStore = new RAMFreenetStore<>(pk, keys); pk.setStore(ramFreenetStore); - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); List sskBlocks = new ArrayList<>(); @@ -994,7 +994,7 @@ void pushOffThreadDelayed_whenDelayExpires_expectFlushedToUnderlyingSSK() PubkeyStore pk = new PubkeyStore(); RAMFreenetStore ramFreenetStore = new RAMFreenetStore<>(pk, keys); pk.setStore(ramFreenetStore); - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); try (SaltedHashFreenetStore saltStore = @@ -1152,7 +1152,7 @@ private void checkOnCollisionsSSK(boolean useSlotFilter) PubkeyStore pk = new PubkeyStore(); RAMFreenetStore ramFreenetStore = new RAMFreenetStore<>(pk, keys); pk.setStore(ramFreenetStore); - GetPubkey pubkeyCache = new SimpleGetPubkey(pk); + GetPubkey pubkeyCache = new SimpleGetPubkey(pk); SSKStore store = new SSKStore(pubkeyCache); File f = getStorePath("checkOnCollisionsSSK");