Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.151.0"
version = "0.151.1"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
2 changes: 1 addition & 1 deletion crates/dig-node-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ name = "dig-node-core"
# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor;
# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x
# rule -- recorded here rather than letting the number imply the locator surface held still.
version = "0.57.0"
version = "0.57.1"
edition = "2021"
license = "GPL-2.0-only"
description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works."
Expand Down
96 changes: 96 additions & 0 deletions crates/dig-node-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7397,6 +7397,102 @@ mod tests {
);
}

/// **Proves (NC-1 / §5.4):** the relay hop pulls and serves a function of `(store_id, root)`
/// ALONE. Recipient-specific material in a requestor's params — a retrieval key, the one field
/// that names WHOM content is for — selects nothing: it does not steer the pull, it does not
/// steer the served bytes, and it leaves no artifact of its own. The hop therefore relays
/// capsule ciphertext it cannot read, exactly as `module_relay`'s module docs claim.
///
/// **Catches:** the widening those docs forbid. Until now that exclusion was PROSE — the epic's
/// own measurement recorded that no test fails if a directed message becomes routable through
/// `module_relay`. The nearest wrong implementation is not a dramatic one: it is a future edit
/// that threads `params["retrieval_key"]` into the relay so the hop can serve a resource rather
/// than a capsule. That single change is what turns a courier of opaque bytes into a courier of
/// recipient-addressed ones, and NC-1 requires those to stay sealed to the recipient key.
///
/// **Why the fixture carries TWO DIFFERENT keys and not one.** A single key is satisfied
/// identically by a hop that ignores it and by a hop that threads it, because with one value
/// there is nothing for a threaded key to select differently — the fixture could not exhibit the
/// property under test. Two different keys across the describe and the window make the two
/// implementations disagree: the correct hop answers both from the same capsule, a threading hop
/// cannot. The inequality is asserted rather than assumed, so a later edit that collapses the two
/// constants silently blinds nothing.
///
/// **Why it runs on the SAME wiring as the gate test above:** a params-insensitivity claim
/// asserted on a hop that refuses everything is vacuous. Here the relay is fully open and
/// genuinely answers, so "the key changed nothing" is a statement about a relay that ran.
#[tokio::test]
async fn a_relay_ignores_recipient_specific_params_entirely() {
let (b, _bd) = test_node(None);
let staging = tempfile::tempdir().unwrap();
let store_raw = [0x8au8; 32];
let (module, root) = chain_anchored_module(store_raw, [0x8bu8; 32]);
let (store, root) = (hex::encode(store_raw), root.to_hex());
let pc = wire_relay_hop(&b, &store, &root, module.clone(), &staging);
pc.set_onion_relay(true);

// The two recipient-specific values the requests carry. Different by construction — that
// difference is the entire discriminating power of this fixture.
let (key_on_describe, key_on_window) = (id_hex(0xc1), id_hex(0xc2));
assert_ne!(
key_on_describe, key_on_window,
"the two keys MUST differ, or this test cannot tell an ignored key from a threaded one"
);

// The DESCRIBE carries one key. It must be answered from the capsule the store/root names.
let described = handle_rpc(
&b,
json!({"jsonrpc":"2.0","id":1,"method":"dig.getModuleInfo","params":{
"store_id": store, "root": root, "proxy": true,
"retrieval_key": key_on_describe,
}}),
crate::download::ReadOrigin::Local,
crate::download::RequestProvenance::FirstParty,
)
.await;
assert_eq!(
described["result"]["total_size"],
json!(module.len() as u64),
"a key-bearing describe is answered by the whole capsule, not by some resource the key \
selected: {described}"
);

// The WINDOW carries a DIFFERENT key. A hop that threaded the key could not answer this from
// the capsule the first request landed; one that ignores it answers byte-identically.
let framed = handle_rpc(
&b,
json!({"jsonrpc":"2.0","id":2,"method":"dig.fetchModuleRange","params":{
"store_id": store, "root": root, "offset": 16, "length": 32, "proxy": true,
"retrieval_key": key_on_window,
}}),
crate::download::ReadOrigin::Local,
crate::download::RequestProvenance::FirstParty,
)
.await;
let decoded: dig_nat::RangeFrame = serde_json::from_value(framed["result"].clone())
.expect("a key-bearing relayed window still decodes as a RangeFrame");
assert_eq!(
decoded.bytes,
module[16..48],
"a second, DIFFERENT key must select nothing — the served bytes are the capsule's"
);

// And the pull left exactly ONE artifact, at the path the capsule identity names. A hop that
// keyed its staging or its cache by the requestor's key would leave a second one here, which
// is what a relayed DIRECTED payload would look like on disk.
assert!(
module_path(&b.cache_dir, &store, &root).exists(),
"the relayed capsule is cached under (store, root)"
);
for key in [&key_on_describe, &key_on_window] {
assert!(
!module_path(&b.cache_dir, &store, key).exists()
&& !module_path(&b.cache_dir, key, &root).exists(),
"no artifact may be keyed by a retrieval key: the hop is never handed one"
);
}
}

/// **Proves:** a non-canonical id on either module method is a -32602 that never reaches the
/// filesystem — a store id concatenated into a path would be a traversal primitive.
#[tokio::test]
Expand Down
6 changes: 6 additions & 0 deletions crates/dig-node-core/src/seams/dig_peer/module_relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
//! the recipient key (NC-1 / §5.4). Widening this path's payload is an NC-1 review, not an
//! extension.
//!
//! That exclusion is FALSIFIABLE rather than merely stated: `a_relay_ignores_recipient_specific_params_entirely`
//! (`crate::tests`) rides two DIFFERENT retrieval keys through a fully-open hop and requires both to
//! select nothing — same capsule described, byte-identical window served, one artifact keyed by
//! `(store, root)`. Verified load-bearing: threading `params["retrieval_key"]` into this function
//! leaves all 988 other tests in the crate green and fails only that one.
//!
//! # The three gates, each independently sufficient to refuse
//!
//! 1. **The requestor asked.** `params.proxy == true`. Automatic relaying is off; a requestor that
Expand Down
Loading