[tor-commits] [onionoo/master] Add "effective_family" field to details documents.

karsten at torproject.org karsten at torproject.org
Sat Aug 8 18:56:55 UTC 2015


commit d3590d9bd48c0d8980360fb476bac9cc7a9f15bd
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Fri Jul 3 10:20:34 2015 +0200

    Add "effective_family" field to details documents.
    
    Implements #16276.
---
 build.xml                                          |    2 +-
 .../torproject/onionoo/docs/DetailsDocument.java   |    8 ++
 .../org/torproject/onionoo/docs/DetailsStatus.java |    8 ++
 .../org/torproject/onionoo/docs/DocumentStore.java |    2 +-
 .../org/torproject/onionoo/docs/NodeStatus.java    |   34 ++++++-
 .../torproject/onionoo/docs/SummaryDocument.java   |   17 +++-
 .../org/torproject/onionoo/server/NodeIndexer.java |   30 ++++--
 .../torproject/onionoo/server/ResponseBuilder.java |    4 +-
 .../onionoo/updater/NodeDetailsStatusUpdater.java  |  102 ++++++++++++++++----
 .../onionoo/writer/DetailsDocumentWriter.java      |    8 ++
 .../onionoo/writer/SummaryDocumentWriter.java      |    3 +-
 .../onionoo/server/ResourceServletTest.java        |   15 ++-
 web/protocol.html                                  |   15 +++
 13 files changed, 209 insertions(+), 39 deletions(-)

diff --git a/build.xml b/build.xml
index b10987b..75dfe1c 100644
--- a/build.xml
+++ b/build.xml
@@ -1,6 +1,6 @@
 <project default="dist" name="onionoo" basedir=".">
 
-  <property name="onionoo.protocol.version" value="2.3"/>
+  <property name="onionoo.protocol.version" value="2.4"/>
   <property name="release.version"
             value="${onionoo.protocol.version}.0"/>
   <property name="javasources" value="src/main/java"/>
diff --git a/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java b/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
index d4efdc0..2b65e50 100644
--- a/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
+++ b/src/main/java/org/torproject/onionoo/docs/DetailsDocument.java
@@ -289,6 +289,14 @@ public class DetailsDocument extends Document {
     return this.family;
   }
 
+  private SortedSet<String> effective_family;
+  public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
+    this.effective_family = effectiveFamily;
+  }
+  public SortedSet<String> getEffectiveFamily() {
+    return this.effective_family;
+  }
+
   private Float consensus_weight_fraction;
   public void setConsensusWeightFraction(Float consensusWeightFraction) {
     if (consensusWeightFraction == null ||
diff --git a/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java b/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
index fe46416..09f0824 100644
--- a/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
+++ b/src/main/java/org/torproject/onionoo/docs/DetailsStatus.java
@@ -110,6 +110,14 @@ public class DetailsStatus extends Document {
     return this.family;
   }
 
+  private SortedSet<String> effective_family;
+  public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
+    this.effective_family = effectiveFamily;
+  }
+  public SortedSet<String> getEffectiveFamily() {
+    return this.effective_family;
+  }
+
   private Map<String, List<String>> exit_policy_v6_summary;
   public void setExitPolicyV6Summary(
       Map<String, List<String>> exitPolicyV6Summary) {
diff --git a/src/main/java/org/torproject/onionoo/docs/DocumentStore.java b/src/main/java/org/torproject/onionoo/docs/DocumentStore.java
index 3be6fdb..8e2162c 100644
--- a/src/main/java/org/torproject/onionoo/docs/DocumentStore.java
+++ b/src/main/java/org/torproject/onionoo/docs/DocumentStore.java
@@ -422,7 +422,7 @@ public class DocumentStore {
     SummaryDocument summaryDocument = new SummaryDocument(isRelay,
         nickname, fingerprint, addresses, lastSeenMillis, running,
         relayFlags, consensusWeight, countryCode, firstSeenMillis,
-        aSNumber, contact, family);
+        aSNumber, contact, family, family);
     return summaryDocument;
   }
 
diff --git a/src/main/java/org/torproject/onionoo/docs/NodeStatus.java b/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
index 0985491..51fc678 100644
--- a/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
+++ b/src/main/java/org/torproject/onionoo/docs/NodeStatus.java
@@ -301,6 +301,16 @@ public class NodeStatus extends Document {
     return this.lastRdnsLookup;
   }
 
+  /* Computed effective family */
+
+  private String[] effectiveFamily;
+  public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
+    this.effectiveFamily = collectionToStringArray(effectiveFamily);
+  }
+  public SortedSet<String> getEffectiveFamily() {
+    return stringArrayToSortedSet(this.effectiveFamily);
+  }
+
   /* Constructor and (de-)serialization methods: */
 
   public NodeStatus(String fingerprint) {
@@ -402,8 +412,17 @@ public class NodeStatus extends Document {
         nodeStatus.setRecommendedVersion(parts[21].equals("true"));
       }
       if (!parts[22].equals("null")) {
-        nodeStatus.setFamilyFingerprints(new TreeSet<String>(
-            Arrays.asList(parts[22].split(";"))));
+        SortedSet<String> familyFingerprints = new TreeSet<String>();
+        for (String familyMember : parts[22].split("[;:]")) {
+          if (familyMember.length() > 0) {
+            familyFingerprints.add(familyMember);
+          }
+        }
+        nodeStatus.setFamilyFingerprints(familyFingerprints);
+        if (parts[22].contains(":")) {
+          nodeStatus.setEffectiveFamily(new TreeSet<String>(
+              Arrays.asList(parts[22].split(":", 2)[1].split(";"))));
+        }
       }
       return nodeStatus;
     } catch (NumberFormatException e) {
@@ -464,7 +483,16 @@ public class NodeStatus extends Document {
     sb.append("\t" + (this.contact != null ? this.contact : ""));
     sb.append("\t" + (this.recommendedVersion == null ? "null" :
         this.recommendedVersion ? "true" : "false"));
-    sb.append("\t" + StringUtils.join(this.familyFingerprints, ";"));
+    if (this.effectiveFamily != null && this.effectiveFamily.length > 0) {
+      SortedSet<String> mutual = this.getEffectiveFamily();
+      SortedSet<String> notMutual = new TreeSet<String>(
+          this.getFamilyFingerprints());
+      notMutual.removeAll(mutual);
+      sb.append("\t" + StringUtils.join(notMutual, ";") + ":"
+          + StringUtils.join(mutual, ";"));
+    } else {
+      sb.append("\t" + StringUtils.join(this.familyFingerprints, ";"));
+    }
     return sb.toString();
   }
 }
diff --git a/src/main/java/org/torproject/onionoo/docs/SummaryDocument.java b/src/main/java/org/torproject/onionoo/docs/SummaryDocument.java
index 8e325f3..ae7202b 100644
--- a/src/main/java/org/torproject/onionoo/docs/SummaryDocument.java
+++ b/src/main/java/org/torproject/onionoo/docs/SummaryDocument.java
@@ -185,6 +185,9 @@ public class SummaryDocument extends Document {
     return this.c;
   }
 
+  /* This attribute can go away once all Onionoo services had their hourly
+   * updater write effective families to summary documents at least once.
+   * Remove this code after September 8, 2015. */
   private String[] ff;
   public void setFamilyFingerprints(
       SortedSet<String> familyFingerprints) {
@@ -194,11 +197,22 @@ public class SummaryDocument extends Document {
     return this.stringArrayToSortedSet(this.ff);
   }
 
+  private String[] ef;
+  public void setEffectiveFamily(SortedSet<String> effectiveFamily) {
+    this.ef = this.collectionToStringArray(effectiveFamily);
+  }
+  public SortedSet<String> getEffectiveFamily() {
+    return this.stringArrayToSortedSet(this.ef);
+  }
+
+  /* The familyFingerprints parameter can go away after September 8, 2015.
+   * See above. */
   public SummaryDocument(boolean isRelay, String nickname,
       String fingerprint, List<String> addresses, long lastSeenMillis,
       boolean running, SortedSet<String> relayFlags, long consensusWeight,
       String countryCode, long firstSeenMillis, String aSNumber,
-      String contact, SortedSet<String> familyFingerprints) {
+      String contact, SortedSet<String> familyFingerprints,
+      SortedSet<String> effectiveFamily) {
     this.setRelay(isRelay);
     this.setNickname(nickname);
     this.setFingerprint(fingerprint);
@@ -212,6 +226,7 @@ public class SummaryDocument extends Document {
     this.setASNumber(aSNumber);
     this.setContact(contact);
     this.setFamilyFingerprints(familyFingerprints);
+    this.setEffectiveFamily(effectiveFamily);
   }
 }
 
diff --git a/src/main/java/org/torproject/onionoo/server/NodeIndexer.java b/src/main/java/org/torproject/onionoo/server/NodeIndexer.java
index 5788d4e..347996b 100644
--- a/src/main/java/org/torproject/onionoo/server/NodeIndexer.java
+++ b/src/main/java/org/torproject/onionoo/server/NodeIndexer.java
@@ -1,7 +1,6 @@
 package org.torproject.onionoo.server;
 
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -19,7 +18,6 @@ import javax.servlet.ServletContextListener;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-
 import org.torproject.onionoo.docs.DocumentStore;
 import org.torproject.onionoo.docs.DocumentStoreFactory;
 import org.torproject.onionoo.docs.SummaryDocument;
@@ -160,6 +158,11 @@ public class NodeIndexer implements ServletContextListener, Runnable {
     }
     Time time = TimeFactory.getTime();
     List<String> orderRelaysByConsensusWeight = new ArrayList<String>();
+    /* This variable can go away once all Onionoo services had their
+     * hourly updater write effective families to summary documents at
+     * least once.  Remove this code after September 8, 2015. */
+    SortedMap<String, Set<String>> computedEffectiveFamilies =
+        new TreeMap<String, Set<String>>();
     for (SummaryDocument entry : currentRelays) {
       String fingerprint = entry.getFingerprint().toUpperCase();
       String hashedFingerprint = entry.getHashedFingerprint().
@@ -196,8 +199,16 @@ public class NodeIndexer implements ServletContextListener, Runnable {
         newRelaysByFlag.get(flagLowerCase).add(fingerprint);
         newRelaysByFlag.get(flagLowerCase).add(hashedFingerprint);
       }
-      if (entry.getFamilyFingerprints() != null) {
-        newRelaysByFamily.put(fingerprint, entry.getFamilyFingerprints());
+      /* This condition can go away once all Onionoo services had their
+       * hourly updater write effective families to summary documents at
+       * least once.  Remove this code after September 8, 2015. */
+      if (entry.getFamilyFingerprints() != null &&
+          !entry.getFamilyFingerprints().isEmpty()) {
+        computedEffectiveFamilies.put(fingerprint,
+            entry.getFamilyFingerprints());
+      }
+      if (entry.getEffectiveFamily() != null) {
+        newRelaysByFamily.put(fingerprint, entry.getEffectiveFamily());
       }
       int daysSinceFirstSeen = (int) ((time.currentTimeMillis()
           - entry.getFirstSeenMillis()) / ONE_DAY);
@@ -229,18 +240,21 @@ public class NodeIndexer implements ServletContextListener, Runnable {
     for (String relay : orderRelaysByConsensusWeight) {
       newRelaysByConsensusWeight.add(relay.split(" ")[1]);
     }
+    /* This loop can go away once all Onionoo services had their hourly
+     * updater write effective families to summary documents at least
+     * once.  Remove this code after September 8, 2015. */
     for (Map.Entry<String, Set<String>> e :
-        newRelaysByFamily.entrySet()) {
+        computedEffectiveFamilies.entrySet()) {
       String fingerprint = e.getKey();
       Set<String> inMutualFamilyRelation = new HashSet<String>();
       for (String otherFingerprint : e.getValue()) {
-        if (newRelaysByFamily.containsKey(otherFingerprint) &&
-            newRelaysByFamily.get(otherFingerprint).contains(
+        if (computedEffectiveFamilies.containsKey(otherFingerprint) &&
+            computedEffectiveFamilies.get(otherFingerprint).contains(
                 fingerprint)) {
           inMutualFamilyRelation.add(otherFingerprint);
         }
       }
-      e.getValue().retainAll(inMutualFamilyRelation);
+      newRelaysByFamily.put(fingerprint, inMutualFamilyRelation);
     }
     for (SummaryDocument entry : currentBridges) {
       String hashedFingerprint = entry.getFingerprint().toUpperCase();
diff --git a/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java b/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
index af0f67e..c7bfd65 100644
--- a/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
+++ b/src/main/java/org/torproject/onionoo/server/ResponseBuilder.java
@@ -70,7 +70,7 @@ public class ResponseBuilder {
     return this.charsWritten;
   }
 
-  private static final String PROTOCOL_VERSION = "2.3";
+  private static final String PROTOCOL_VERSION = "2.4";
 
   private static final String NEXT_MAJOR_VERSION_SCHEDULED = null;
 
@@ -267,6 +267,8 @@ public class ResponseBuilder {
             dd.setHibernating(detailsDocument.getHibernating());
           } else if (field.equals("transports")) {
             dd.setTransports(detailsDocument.getTransports());
+          } else if (field.equals("effective_family")) {
+            dd.setEffectiveFamily(detailsDocument.getEffectiveFamily());
           }
         }
         /* Don't escape HTML characters, like < and >, contained in
diff --git a/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java b/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
index be2bd9a..4fb0143 100644
--- a/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
+++ b/src/main/java/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java
@@ -56,14 +56,14 @@ import org.torproject.onionoo.util.TimeFactory;
  *      are not loaded from disk before the parse step in order to save
  *      memory for parsed descriptors.
  *   3. Perform reverse DNS lookups, Look up relay IP addresses in a
- *      GeoIP database, and calculate path selection probabilities.
- *      Update node statuses accordingly.
+ *      GeoIP database, calculate path selection probabilities, and
+ *      compute effective families, and update node statuses accordingly.
  *   4. Retrieve details statuses corresponding to nodes that have been
  *      changed since the start of the update process, possibly update the
  *      node statuses with contents from newly parsed descriptors, update
- *      details statuses with results from lookup operations and new path
- *      selection probabilities, and store details statuses and node
- *      statuses back to disk.
+ *      details statuses with results from lookup operations, new path
+ *      selection probabilities, and effective families, and store details
+ *      statuses and node statuses back to disk.
  */
 public class NodeDetailsStatusUpdater implements DescriptorListener,
     StatusUpdater {
@@ -140,6 +140,9 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
     }
   }
 
+  private Map<String, SortedSet<String>> familyFingerprints =
+      new HashMap<String, SortedSet<String>>();
+
   private void processRelayServerDescriptor(
       ServerDescriptor descriptor) {
     String fingerprint = descriptor.getFingerprint();
@@ -170,6 +173,16 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
     detailsStatus.setContact(descriptor.getContact());
     detailsStatus.setPlatform(descriptor.getPlatform());
     detailsStatus.setFamily(descriptor.getFamilyEntries());
+    if (descriptor.getFamilyEntries() != null) {
+      SortedSet<String> noPrefixUpperCase = new TreeSet<String>();
+      for (String familyMember : descriptor.getFamilyEntries()) {
+        if (familyMember.startsWith("$") && familyMember.length() >= 41) {
+          noPrefixUpperCase.add(
+              familyMember.substring(1, 41).toUpperCase());
+        }
+      }
+      this.familyFingerprints.put(fingerprint, noPrefixUpperCase);
+    }
     if (descriptor.getIpv6DefaultPolicy() != null &&
         (descriptor.getIpv6DefaultPolicy().equals("accept") ||
         descriptor.getIpv6DefaultPolicy().equals("reject")) &&
@@ -354,6 +367,8 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
     log.info("Looked up cities and ASes");
     this.calculatePathSelectionProbabilities();
     log.info("Calculated path selection probabilities");
+    this.computeEffectiveFamilies();
+    log.info("Computed effective families");
     this.finishReverseDomainNameLookups();
     log.info("Finished reverse domain name lookups");
     this.updateNodeDetailsStatuses();
@@ -449,6 +464,10 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
         }
         updatedNodeStatus.setLastRdnsLookup(
             nodeStatus.getLastRdnsLookup());
+        updatedNodeStatus.setFamilyFingerprints(
+            nodeStatus.getFamilyFingerprints());
+        updatedNodeStatus.setEffectiveFamily(
+            nodeStatus.getEffectiveFamily());
       } else {
         updatedNodeStatus = nodeStatus;
         this.knownNodes.put(fingerprint, nodeStatus);
@@ -471,6 +490,19 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
         }
       }
     }
+    /* Update family fingerprints in known nodes with any fingerprints we
+     * learned when parsing server descriptors in this run.  These are
+     * guaranteed to come from more recent server descriptors, so it's
+     * safe to override whatever is in node statuses. */
+    for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) {
+      String fingerprint = e.getKey();
+      if (this.familyFingerprints.containsKey(fingerprint)) {
+        NodeStatus nodeStatus = e.getValue();
+        nodeStatus.setFamilyFingerprints(
+            this.familyFingerprints.get(fingerprint));
+      }
+    }
+    this.familyFingerprints.clear();
   }
 
   /* Step 3: perform lookups and calculate path selection
@@ -617,6 +649,52 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
     }
   }
 
+  private void computeEffectiveFamilies() {
+    SortedMap<String, SortedSet<String>> declaredFamilies =
+        new TreeMap<String, SortedSet<String>>();
+    for (String fingerprint : this.currentRelays) {
+      NodeStatus nodeStatus = this.knownNodes.get(fingerprint);
+      if (nodeStatus != null &&
+          nodeStatus.getFamilyFingerprints() != null &&
+          !nodeStatus.getFamilyFingerprints().isEmpty()) {
+        declaredFamilies.put(fingerprint,
+            nodeStatus.getFamilyFingerprints());
+      }
+    }
+    SortedMap<String, SortedSet<String>> effectiveFamilies =
+        new TreeMap<String, SortedSet<String>>();
+    for (Map.Entry<String, SortedSet<String>> e :
+        declaredFamilies.entrySet()) {
+      String fingerprint = e.getKey();
+      SortedSet<String> declaredFamily = e.getValue();
+      SortedSet<String> effectiveFamily = new TreeSet<String>();
+      for (String declaredFamilyMember : declaredFamily) {
+        if (declaredFamilies.containsKey(declaredFamilyMember) &&
+            declaredFamilies.get(declaredFamilyMember).contains(
+            fingerprint)) {
+          effectiveFamily.add(declaredFamilyMember);
+        }
+      }
+      if (!effectiveFamily.isEmpty()) {
+        effectiveFamilies.put(fingerprint, effectiveFamily);
+      }
+    }
+    for (String fingerprint : this.currentRelays) {
+      NodeStatus nodeStatus = this.knownNodes.get(fingerprint);
+      if (nodeStatus == null) {
+        continue;
+      }
+      if (effectiveFamilies.containsKey(fingerprint)) {
+        nodeStatus.setEffectiveFamily(effectiveFamilies.get(fingerprint));
+        this.updatedNodes.add(fingerprint);
+      } else if (nodeStatus.getEffectiveFamily() != null ||
+          !nodeStatus.getEffectiveFamily().isEmpty()) {
+        nodeStatus.setEffectiveFamily(null);
+        this.updatedNodes.add(fingerprint);
+      }
+    }
+  }
+
   private long startedRdnsLookups = -1L;
 
   private SortedMap<String, String> rdnsLookupResults =
@@ -681,19 +759,7 @@ public class NodeDetailsStatusUpdater implements DescriptorListener,
           nodeStatus.getOrAddresses());
       nodeStatus.setExitAddresses(exitAddressesWithoutOrAddresses);
 
-      if (detailsStatus.getFamily() != null &&
-          !detailsStatus.getFamily().isEmpty()) {
-        SortedSet<String> familyFingerprints = new TreeSet<String>();
-        for (String familyMember : detailsStatus.getFamily()) {
-          if (familyMember.startsWith("$") &&
-              familyMember.length() == 41) {
-            familyFingerprints.add(familyMember.substring(1));
-          }
-        }
-        if (!familyFingerprints.isEmpty()) {
-          nodeStatus.setFamilyFingerprints(familyFingerprints);
-        }
-      }
+      detailsStatus.setEffectiveFamily(nodeStatus.getEffectiveFamily());
 
       if (this.geoIpLookupResults.containsKey(fingerprint)) {
         LookupResult lookupResult = this.geoIpLookupResults.get(
diff --git a/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java b/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
index 1a1ddc3..152b4cb 100644
--- a/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
+++ b/src/main/java/org/torproject/onionoo/writer/DetailsDocumentWriter.java
@@ -112,6 +112,14 @@ public class DetailsDocumentWriter implements DocumentWriter {
     detailsDocument.setContact(detailsStatus.getContact());
     detailsDocument.setPlatform(detailsStatus.getPlatform());
     detailsDocument.setFamily(detailsStatus.getFamily());
+    if (detailsStatus.getEffectiveFamily() != null &&
+        !detailsStatus.getEffectiveFamily().isEmpty()) {
+      SortedSet<String> effectiveFamily = new TreeSet<String>();
+      for (String familyMember : detailsStatus.getEffectiveFamily()) {
+        effectiveFamily.add("$" + familyMember);
+      }
+      detailsDocument.setEffectiveFamily(effectiveFamily);
+    }
     detailsDocument.setExitPolicyV6Summary(
         detailsStatus.getExitPolicyV6Summary());
     detailsDocument.setHibernating(detailsStatus.getHibernating());
diff --git a/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java b/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
index 6406a28..ddb3003 100644
--- a/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
+++ b/src/main/java/org/torproject/onionoo/writer/SummaryDocumentWriter.java
@@ -86,10 +86,11 @@ public class SummaryDocumentWriter implements DocumentWriter {
       String contact = nodeStatus.getContact();
       SortedSet<String> familyFingerprints =
           nodeStatus.getFamilyFingerprints();
+      SortedSet<String> effectiveFamily = nodeStatus.getEffectiveFamily();
       SummaryDocument summaryDocument = new SummaryDocument(isRelay,
           nickname, fingerprint, addresses, lastSeenMillis, running,
           relayFlags, consensusWeight, countryCode, firstSeenMillis,
-          aSNumber, contact, familyFingerprints);
+          aSNumber, contact, familyFingerprints, effectiveFamily);
       if (this.documentStore.store(summaryDocument, fingerprint)) {
         this.writtenDocuments++;
       };
diff --git a/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java b/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java
index c6ef9c8..f52413a 100644
--- a/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java
+++ b/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java
@@ -128,7 +128,9 @@ public class ResourceServletTest {
         "torkaz <klaus dot zufall at gmx dot de> "
         + "<fb-token:np5_g_83jmf=>", new TreeSet<String>(Arrays.asList(
         new String[] { "001C13B3A55A71B977CA65EC85539D79C653A3FC",
-        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B" })));
+        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B" })),
+        new TreeSet<String>(Arrays.asList(
+        new String[] { "001C13B3A55A71B977CA65EC85539D79C653A3FC" })));
     org.torproject.onionoo.docs.SummaryDocument relayFerrari458 =
         new org.torproject.onionoo.docs.SummaryDocument(true, "Ferrari458",
         "001C13B3A55A71B977CA65EC85539D79C653A3FC", Arrays.asList(
@@ -138,6 +140,8 @@ public class ResourceServletTest {
         "Running", "V2Dir", "Valid" })), 1140L, "us",
         DateTimeHelper.parse("2013-02-12 16:00:00"), "AS7922", null,
         new TreeSet<String>(Arrays.asList(new String[] {
+        "000C5F55BD4814B917CC474BD537F1A3B33CCE2A" })),
+        new TreeSet<String>(Arrays.asList(new String[] {
         "000C5F55BD4814B917CC474BD537F1A3B33CCE2A" })));
     org.torproject.onionoo.docs.SummaryDocument relayTimMayTribute =
         new org.torproject.onionoo.docs.SummaryDocument(true, "TimMayTribute",
@@ -149,7 +153,7 @@ public class ResourceServletTest {
         DateTimeHelper.parse("2013-04-16 18:00:00"), "AS6830",
         "1024D/51E2A1C7 steven j. murdoch "
         + "<tor+steven.murdoch at cl.cam.ac.uk> <fb-token:5sr_k_zs2wm=>",
-        new TreeSet<String>());
+        new TreeSet<String>(), new TreeSet<String>());
     org.torproject.onionoo.docs.SummaryDocument bridgeec2bridgercc7f31fe =
         new org.torproject.onionoo.docs.SummaryDocument(false,
         "ec2bridgercc7f31fe", "0000831B236DFF73D409AD17B40E2A728A53994F",
@@ -157,7 +161,7 @@ public class ResourceServletTest {
         DateTimeHelper.parse("2013-04-21 18:07:03"), false,
         new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
         null, DateTimeHelper.parse("2013-04-20 15:37:04"), null, null,
-        null);
+        null, null);
     org.torproject.onionoo.docs.SummaryDocument bridgeUnnamed =
         new org.torproject.onionoo.docs.SummaryDocument(false, "Unnamed",
         "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C", Arrays.asList(
@@ -165,7 +169,7 @@ public class ResourceServletTest {
         DateTimeHelper.parse("2013-04-20 17:37:04"), false,
         new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
         null, DateTimeHelper.parse("2013-04-14 07:07:05"), null, null,
-        null);
+        null, null);
     org.torproject.onionoo.docs.SummaryDocument bridgegummy =
         new org.torproject.onionoo.docs.SummaryDocument(false, "gummy",
         "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", Arrays.asList(
@@ -173,7 +177,8 @@ public class ResourceServletTest {
         DateTimeHelper.parse("2013-04-24 01:07:04"), true,
         new TreeSet<String>(Arrays.asList(new String[] { "Running",
         "Valid" })), -1L, null,
-        DateTimeHelper.parse("2013-01-16 21:07:04"), null, null, null);
+        DateTimeHelper.parse("2013-01-16 21:07:04"), null, null, null,
+        null);
     this.relays =
         new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>();
     this.relays.put("000C5F55BD4814B917CC474BD537F1A3B33CCE2A",
diff --git a/web/protocol.html b/web/protocol.html
index 00221b0..9f36e4d 100644
--- a/web/protocol.html
+++ b/web/protocol.html
@@ -176,6 +176,8 @@ added "transports" field to bridge details documents on December 8,
 2014.</li>
 <li><strong>2.3</strong>: Added optional "flags" field to uptime
 documents on March 22, 2015.</li>
+<li><strong>2.4</strong>: Added optional "effective_family" field to
+details documents on July 3, 2015.</li>
 </ul>
 
 </div> <!-- box -->
@@ -1185,6 +1187,19 @@ found.
 </li>
 
 <li>
+<b><font color="blue">effective_family</font></b>
+<code class="typeof">array of strings</code>
+<span class="required-false">optional</span>
+<p>
+Array of $-prefixed fingerprints of relays that are in an effective,
+mutual family relationship with this relay.
+Omitted if empty or if descriptor containing this information cannot be
+found.
+<font color="blue">Added on July 3, 2015.</font>
+</p>
+</li>
+
+<li>
 <b>consensus_weight_fraction</b>
 <code class="typeof">number</code>
 <span class="required-false">optional</span>



More information about the tor-commits mailing list