From f218ec7d470d5813f5ff9bd38b941fb41b7ac388 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:18:17 +0000 Subject: [PATCH 1/2] refactor(store-contracts): Extract neutral store contracts leaf Move the neutral store contracts into :foundation-store-contracts while keeping the network.crypta.store package names intact. Genericize GetPubkey to remove the contracts leaf's dependency on DSAPublicKey, update the affected production and test call sites, and wire the new leaf into Gradle selective ownership metadata. --- build.gradle.kts | 23 +++--- foundation-store-contracts/build.gradle.kts | 6 ++ .../gradle/owned-output-patterns.txt | 6 ++ .../network/crypta/store/BlockMetadata.java | 35 +++++++++ .../java/network/crypta/store/GetPubkey.java | 44 +++++++++++ .../network/crypta/store/StorableBlock.java | 31 ++++++++ settings.gradle.kts | 1 + .../java/network/crypta/keys/NodeSSK.java | 10 ++- .../network/crypta/node/NodeGetPubkey.java | 4 +- .../network/crypta/store/BlockMetadata.java | 51 ------------- .../java/network/crypta/store/GetPubkey.java | 75 ------------------- .../java/network/crypta/store/SSKStore.java | 4 +- .../network/crypta/store/SimpleGetPubkey.java | 2 +- .../network/crypta/store/StorableBlock.java | 46 ------------ .../java/network/crypta/keys/NodeSSKTest.java | 35 ++++++--- .../network/crypta/store/SSKStoreTest.java | 2 +- .../crypta/store/SimplePubkeyCacheTest.java | 5 +- .../caching/CachingFreenetStoreTest.java | 14 ++-- 18 files changed, 183 insertions(+), 211 deletions(-) create mode 100644 foundation-store-contracts/build.gradle.kts create mode 100644 foundation-store-contracts/gradle/owned-output-patterns.txt create mode 100644 foundation-store-contracts/src/main/java/network/crypta/store/BlockMetadata.java create mode 100644 foundation-store-contracts/src/main/java/network/crypta/store/GetPubkey.java create mode 100644 foundation-store-contracts/src/main/java/network/crypta/store/StorableBlock.java delete mode 100644 src/main/java/network/crypta/store/BlockMetadata.java delete mode 100644 src/main/java/network/crypta/store/GetPubkey.java delete mode 100644 src/main/java/network/crypta/store/StorableBlock.java 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"); From 832b124ac2aebee61a3da84fdb9bbe39712535a4 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:18:42 +0000 Subject: [PATCH 2/2] docs(modules): Refresh extracted leaf documentation Update the README and the build, architecture, and packaging skills to reflect the current extracted leaf set. Document foundation-support and foundation-store-contracts, clarify config leaf exports, and refresh the quick-check guidance for the partial multi-project build. --- .agents/skills/cryptad-architecture/SKILL.md | 38 +++++++++----- .agents/skills/cryptad-build-test/SKILL.md | 20 ++++++-- .agents/skills/cryptad-packaging/SKILL.md | 10 ++-- README.md | 54 +++++++++++++------- 4 files changed, 80 insertions(+), 42 deletions(-) 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`.