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
18 changes: 4 additions & 14 deletions src/main/java/network/crypta/node/Location.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package network.crypta.node;

import network.crypta.support.math.KeyspaceMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -107,7 +108,7 @@ public static double distanceAllowInvalid(double a, double b) {
* @return the minimal arc length in {@code [0.0, 0.5]}
*/
private static double simpleDistance(double a, double b) {
return Math.abs(change(a, b));
return KeyspaceMath.distance(a, b);
}

/**
Expand All @@ -122,14 +123,7 @@ private static double simpleDistance(double a, double b) {
* @return the signed delta on the unit circle
*/
public static double change(double from, double to) {
double change = to - from;
if (change > 0.5) {
return change - 1.0;
}
if (change <= -0.5) {
return change + 1.0;
}
return change;
return KeyspaceMath.change(from, to);
}

/**
Expand All @@ -145,11 +139,7 @@ public static double change(double from, double to) {
* @return the normalized location in {@code [0.0, 1.0)}
*/
public static double normalize(double rough) {
double normal = rough % 1.0;
if (normal < 0) {
return 1.0 + normal;
}
return normal;
return KeyspaceMath.normalize(rough);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package network.crypta.node;

import network.crypta.support.math.TimeSkewAlertCallback;

/**
* Callback interface for reporting detected local clock skew.
*
* <p>Implementations surface a user-facing alert when the system clock appears out of sync. The
* exact presentation is application-specific (for example, UI banner, status panel entry, or a
* prominent log message). Callers may invoke this callback from non-UI threads; implementations
* that interact with UI toolkits must marshal to the appropriate thread.
* that interact with UI toolkits must be marshaled to the appropriate thread.
*
* <p>This interface does not prescribe how often the alert is shown. Implementations should avoid
* repeatedly notifying the user if the condition persists.
*
* @author Florent Daigni&egrave;re &lt;nextgens@freenetproject.org&gt;
*/
public interface TimeSkewDetectorCallback {
public interface TimeSkewDetectorCallback extends TimeSkewAlertCallback {

/**
* Requests that the implementation present a user-visible alert indicating that time skew has
Expand All @@ -22,5 +24,6 @@ public interface TimeSkewDetectorCallback {
* <p>Side effects depend on the concrete implementation. The call should return promptly; any
* long-running work or UI updates should be scheduled asynchronously.
*/
@Override
void setTimeSkewDetectedUserAlert();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package network.crypta.support.math;

import java.io.Serial;
import network.crypta.node.Location;
import network.crypta.support.SimpleFieldSet;

/**
Expand Down Expand Up @@ -38,7 +37,7 @@ public final class DecayingKeyspaceAverage implements RunningAverage {
/**
* Creates a new keyspace-aware decaying running average.
*
* @param defaultValue initial normalized value returned by {@link #currentValue()} before any
* @param defaultValue initially normalized value returned by {@link #currentValue()} before any
* valid report. Expected in {@code [0.0, 1.0]}.
* @param maxReports number of valid reports before decay stabilizes at {@code 1/maxReports}.
* Larger values decay more slowly.
Expand Down Expand Up @@ -92,7 +91,7 @@ public synchronized double currentValue() {
* Reports a value in the normalized keyspace range {@code [0.0, 1.0]}.
*
* <p>To handle the wrap at {@code 1.0/0.0}, this method computes an unwrapped delta from the
* current normalized average to {@code d} using {@link Location#change(double, double)} and
* current normalized average to {@code d} using {@link KeyspaceMath#change(double, double)} and
* updates the underlying average in that unwrapped space. After the update it normalizes the
* stored value back into {@code [0.0, 1.0)}.
*
Expand All @@ -107,12 +106,12 @@ public synchronized void report(double d) {
// Using an unwrapped representation does not relax the input contract here.
throw new IllegalArgumentException("Not a valid normalized key: " + d);
double superValue = avg.currentValue();
double thisValue = Location.normalize(superValue);
double diff = Location.change(thisValue, d);
double thisValue = KeyspaceMath.normalize(superValue);
double diff = KeyspaceMath.change(thisValue, d);
double toAverage = (superValue + diff);
avg.report(toAverage);
// Normalize the stored value back into [0.0, 1.0), so exactly 1.0 becomes 0.0.
avg.setCurrentValue(Location.normalize(avg.currentValue()));
avg.setCurrentValue(KeyspaceMath.normalize(avg.currentValue()));
}

/**
Expand All @@ -129,9 +128,9 @@ public synchronized double valueIfReported(double d) {
if ((d < 0.0) || (d > 1.0) || Double.isNaN(d) || Double.isInfinite(d))
throw new IllegalArgumentException("Not a valid normalized key: " + d);
double superValue = avg.currentValue();
double thisValue = Location.normalize(superValue);
double diff = Location.change(thisValue, d);
return Location.normalize(avg.valueIfReported(superValue + diff));
double thisValue = KeyspaceMath.normalize(superValue);
double diff = KeyspaceMath.change(thisValue, d);
return KeyspaceMath.normalize(avg.valueIfReported(superValue + diff));
}

/**
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/network/crypta/support/math/KeyspaceMath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package network.crypta.support.math;

/**
* Performs wrap-aware arithmetic on Cryptad's normalized unit keyspace.
*
* <p>This helper centralizes the circular math shared by support-side averages and node-side
* location utilities such as {@code network.crypta.node.Location}. Callers use it when they already
* enforce their own validity checks and need the historical wrap behavior at the {@code 0.0}/{@code
* 1.0} boundary. The methods are pure, allocate no state, and keep the long-standing convention
* that diametrically opposite points produce a positive half-turn.
*
* <p>The class is intentionally narrow:
*
* <ul>
* <li>{@link #normalize(double)} maps arbitrary doubles into the canonical storage range.
* <li>{@link #change(double, double)} returns the signed shortest path across the ring.
* <li>{@link #distance(double, double)} derives the absolute arc length from the same rule set.
* </ul>
*
* <p>By keeping these semantics in one place, support-layer code such as {@code
* DecayingKeyspaceAverage} can use the same behavior as node-layer location code without depending
* on node-specific APIs.
*/
public final class KeyspaceMath {

/** Prevents instantiation of this stateless helper class. */
private KeyspaceMath() {}

/**
* Returns the shortest signed delta from one normalized position to another.
*
* <p>Use this when a caller needs to move from a current keyspace position toward a target, and
* the shortest route may cross the wrap boundary. The result matches Cryptad's historical
* location semantics: values greater than {@code 0.5} wrap backward by {@code 1.0}, values at or
* below {@code -0.5} wrap forward by {@code 1.0}, and exactly opposite points produce {@code
* +0.5}. The method does not validate inputs, so callers should normalize or validate first when
* range guarantees matter.
*
* @param from normalized starting position supplied by the caller.
* @param to normalized destination position supplied by the caller.
* @return a signed shortest-path delta on the unit keyspace ring.
*/
public static double change(double from, double to) {
double change = to - from;
if (change > 0.5) {
return change - 1.0;
}
if (change <= -0.5) {
return change + 1.0;
}
return change;
}

/**
* Returns the shortest absolute arc length between two normalized positions.
*
* <p>This is a convenience wrapper around {@link #change(double, double)} for callers that need
* only the scale of the shortest route. The computation remains symmetric for any ordered pair
* because it derives from the same signed wrap rules. Like the other helpers in this class, it
* assumes the caller has already decided whether the inputs are valid keyspace coordinates.
*
* @param a first normalized keyspace position to compare.
* @param b second normalized keyspace position to compare.
* @return shortest absolute distance measured on the unit ring.
*/
public static double distance(double a, double b) {
return Math.abs(change(a, b));
}

/**
* Maps an arbitrary double into Cryptad's canonical keyspace range.
*
* <p>The returned value is always in {@code [0.0, 1.0)}, which makes it suitable for storage,
* comparison, and further calls to {@link #change(double, double)} or {@link #distance(double,
* double)}. Multiples of {@code 1.0} collapse onto the same position, so {@code 1.0} becomes
* {@code 0.0}. The implementation intentionally follows Java remainder rules and then adjusts
* negative results, preserving the behavior historically exposed through the node-side {@code
* Location.normalize(double)} helper.
*
* @param rough arbitrary keyspace value that may need wrapping.
* @return equivalent normalized position in the canonical unit range.
*/
public static double normalize(double rough) {
double normal = rough % 1.0;
if (normal < 0) {
return 1.0 + normal;
}
return normal;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.function.LongSupplier;
import network.crypta.node.TimeSkewDetectorCallback;
import network.crypta.support.SimpleFieldSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -128,7 +127,7 @@ public final class TimeDecayingRunningAverage implements RunningAverage {
*/
double maxReport;

private final transient TimeSkewDetectorCallback timeSkewCallback;
private final transient TimeSkewAlertCallback timeSkewCallback;
private transient LongSupplier wallClockTimeSourceMillis;
private transient LongSupplier monotonicTimeSourceNanos;

Expand Down Expand Up @@ -195,7 +194,7 @@ public String toString() {
* @param callback optional callback notified when wall‑clock time regresses; may be {@code null}
*/
public TimeDecayingRunningAverage(
RunningAverageBounds bounds, long halfLife, TimeSkewDetectorCallback callback) {
RunningAverageBounds bounds, long halfLife, TimeSkewAlertCallback callback) {

curValue = bounds.defaultValue();
this.defaultValue = bounds.defaultValue();
Expand Down Expand Up @@ -232,7 +231,7 @@ public TimeDecayingRunningAverage(
RunningAverageBounds bounds,
long halfLife,
SimpleFieldSet fs,
TimeSkewDetectorCallback callback) {
TimeSkewAlertCallback callback) {

curValue = bounds.defaultValue();
this.defaultValue = bounds.defaultValue();
Expand Down Expand Up @@ -286,7 +285,7 @@ public TimeDecayingRunningAverage(
RunningAverageBounds bounds,
long halfLife,
SimpleFieldSet fs,
TimeSkewDetectorCallback callback,
TimeSkewAlertCallback callback,
LongSupplier wallClockTimeSourceMillis,
LongSupplier monotonicTimeSourceNanos) {

Expand Down Expand Up @@ -352,7 +351,7 @@ public TimeDecayingRunningAverage(
RunningAverageBounds bounds,
double halfLife,
DataInputStream dis,
TimeSkewDetectorCallback callback)
TimeSkewAlertCallback callback)
throws IOException {
int m = dis.readInt();
if (m != MAGIC) throw new IOException("Invalid magic " + m);
Expand Down Expand Up @@ -419,7 +418,7 @@ private static final class Snapshot {
boolean started;
long totalReports;
double curValue;
TimeSkewDetectorCallback timeSkewCallback;
TimeSkewAlertCallback timeSkewCallback;
LongSupplier wallClockTimeSourceMillis;
LongSupplier monotonicTimeSourceNanos;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package network.crypta.support.math;

/**
* Receives notifications when support-side time checks detect a local clock skew.
*
* <p>This interface defines the narrow contract that support-layer components use when they need to
* surface a user-visible time-skew condition without depending on node-specific alerting code. In
* practice, callers such as {@code TimeDecayingRunningAverage} invoke it after detecting wall-clock
* or uptime regressions while continuing their monotonic-time calculations. That keeps timing math
* isolated from presentation concerns while still giving higher layers a clear place to register
* operator-facing alerts.
*
* <p>Implementations may map the notification to a UI banner, a persistent user alert, or another
* operator-facing mechanism. Callers may invoke the callback from non-UI threads and may notify
* more than once if skew is detected repeatedly, so implementations should return quickly and
* perform any deduplication or thread marshaling they require.
*/
public interface TimeSkewAlertCallback {

/**
* Requests an operator-visible alert for a detected local time-skew condition.
*
* <p>Callers use this after they have already determined that the local wall clock moved backward
* or otherwise became inconsistent with elapsed monotonic time. The callback is
* notification-only: it does not influence the caller's timing calculations, and it does not
* imply that the condition has cleared. Implementations may schedule UI work or alert
* registration, but they should keep the call itself lightweight so reporting paths remain
* responsive.
*/
void setTimeSkewDetectedUserAlert();
}
21 changes: 20 additions & 1 deletion src/test/java/network/crypta/node/LocationTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package network.crypta.node;

import network.crypta.support.math.KeyspaceMath;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -19,7 +20,7 @@ class LocationTest {
// Maximal acceptable difference to consider two doubles equal.
private static final double EPSILON = 1e-12;

// Just some valid non corner case locations.
// Just some valid non-corner case locations.
private static final double VALID_1 = 0.2;
private static final double VALID_2 = 0.75;

Expand Down Expand Up @@ -118,6 +119,24 @@ void normalize_whenOffsetsApplied_expectValueWrappedIntoRange() {
assertEquals(0.0, Location.normalize(1.0), EPSILON);
}

@ParameterizedTest
@CsvSource({"-1.75", "-0.25", "0.0", "0.2", "1.0", "1.75"})
void normalize_whenDelegated_expectSameAsKeyspaceMath(double input) {
assertEquals(KeyspaceMath.normalize(input), Location.normalize(input), EPSILON);
}

@ParameterizedTest
@CsvSource({"0.2, 0.75", "0.75, 0.2", "0.9, 0.1", "0.1, 0.9", "0.0, 0.5", "0.5, 0.0"})
void change_whenDelegated_expectSameAsKeyspaceMath(double from, double to) {
assertEquals(KeyspaceMath.change(from, to), Location.change(from, to), EPSILON);
}

@ParameterizedTest
@CsvSource({"0.2, 0.75", "0.75, 0.2", "0.9, 0.1", "0.1, 0.9", "0.0, 0.5", "0.25, 0.25"})
void distance_whenDelegated_expectSameAsKeyspaceMath(double from, double to) {
assertEquals(KeyspaceMath.distance(from, to), Location.distance(from, to), EPSILON);
}

@Test
void distanceAllowInvalid_whenAnyInvalid_expectSpecifiedSemantics() {
// Simple cases.
Expand Down
Loading
Loading