You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by be...@apache.org on 2023/01/17 15:00:24 UTC

[cassandra-accord] branch trunk updated (bf414d3 -> d9cb3b5)

This is an automated email from the ASF dual-hosted git repository.

benedict pushed a change to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git


    omit bf414d3  Refactor Timestamp/TxnId (#22)
    omit 53be172  Triage TODOs (#24)
    omit 98cc3a5  Shard CommandStores by contiguous ranges (#20)
     new 804a77d  Shard CommandStores by contiguous ranges (#20)
     new 2c54a63  Triage TODOs (#24)
     new d9cb3b5  Refactor Timestamp/TxnId (#22)

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (bf414d3)
            \
             N -- N -- N   refs/heads/trunk (d9cb3b5)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/accord/local/ShardDistributor.java   | 64 ++++++----------------
 .../src/test/java/accord/impl/basic/Cluster.java   |  2 +-
 .../test/java/accord/impl/mock/MockCluster.java    |  2 +-
 .../src/test/java/accord/local/CommandTest.java    |  2 +-
 .../test/java/accord/messages/PreAcceptTest.java   |  2 +-
 .../src/main/java/accord/maelstrom/Cluster.java    |  2 +-
 .../src/main/java/accord/maelstrom/Main.java       |  2 +-
 7 files changed, 24 insertions(+), 52 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org


[cassandra-accord] 02/03: Triage TODOs (#24)

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

benedict pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git

commit 2c54a63cd90950b97eb8df26958e1fb2566cd0fd
Author: Benedict Elliott Smith <be...@apache.org>
AuthorDate: Wed Jan 11 12:10:08 2023 +0000

    Triage TODOs (#24)
---
 accord-core/src/main/java/accord/api/Result.java   |  2 +-
 accord-core/src/main/java/accord/api/Write.java    |  2 +-
 .../src/main/java/accord/coordinate/CheckOn.java   |  4 +-
 .../main/java/accord/coordinate/CheckShards.java   |  2 +-
 .../main/java/accord/coordinate/Coordinate.java    | 19 ++++----
 .../src/main/java/accord/coordinate/FetchData.java |  2 +-
 .../java/accord/coordinate/InformHomeOfTxn.java    |  4 +-
 .../main/java/accord/coordinate/Invalidate.java    | 20 ++++-----
 .../main/java/accord/coordinate/MaybeRecover.java  |  4 +-
 .../src/main/java/accord/coordinate/Persist.java   |  6 +--
 .../java/accord/coordinate/ReadCoordinator.java    |  8 ++--
 .../src/main/java/accord/coordinate/Recover.java   | 17 ++++----
 .../java/accord/coordinate/RecoverWithRoute.java   |  2 +-
 .../accord/coordinate/tracking/ReadTracker.java    | 12 +++---
 .../java/accord/impl/InMemoryCommandStore.java     |  7 +--
 .../main/java/accord/impl/SimpleProgressLog.java   | 33 +++++++-------
 .../src/main/java/accord/local/Command.java        | 50 +++++++++-------------
 .../src/main/java/accord/local/CommandsForKey.java |  6 +--
 accord-core/src/main/java/accord/local/Node.java   | 21 +++------
 .../src/main/java/accord/local/PartialCommand.java | 27 ------------
 .../src/main/java/accord/local/PreLoadContext.java |  2 +-
 .../src/main/java/accord/local/SaveStatus.java     |  6 +--
 .../main/java/accord/local/ShardDistributor.java   |  4 +-
 accord-core/src/main/java/accord/local/Status.java |  5 ++-
 .../main/java/accord/local/SyncCommandStores.java  |  2 +-
 .../src/main/java/accord/messages/Accept.java      |  5 ++-
 .../src/main/java/accord/messages/Apply.java       |  1 -
 .../main/java/accord/messages/BeginRecovery.java   |  8 ++--
 .../src/main/java/accord/messages/Callback.java    |  3 +-
 .../src/main/java/accord/messages/Commit.java      | 13 +++---
 .../src/main/java/accord/messages/Defer.java       | 11 +++--
 .../src/main/java/accord/messages/GetDeps.java     |  1 -
 .../main/java/accord/messages/InformDurable.java   |  6 +--
 .../java/accord/messages/InformHomeDurable.java    |  2 +-
 .../main/java/accord/messages/InformOfTxnId.java   |  6 +--
 .../src/main/java/accord/messages/PreAccept.java   |  2 +-
 .../src/main/java/accord/messages/ReadData.java    | 14 +++---
 .../src/main/java/accord/messages/TxnRequest.java  | 20 ++++-----
 .../main/java/accord/primitives/AbstractKeys.java  |  7 ++-
 .../java/accord/primitives/AbstractRanges.java     |  9 ++--
 .../src/main/java/accord/primitives/Deps.java      | 23 +++++-----
 .../main/java/accord/primitives/FullKeyRoute.java  |  8 ----
 .../src/main/java/accord/primitives/Keys.java      |  3 +-
 .../java/accord/primitives/PartialKeyRoute.java    |  7 ---
 .../java/accord/primitives/PartialRangeRoute.java  |  2 -
 .../main/java/accord/primitives/PartialTxn.java    |  7 +--
 .../main/java/accord/primitives/ProgressToken.java |  1 -
 .../main/java/accord/primitives/RoutableKey.java   |  2 +-
 .../src/main/java/accord/topology/Shard.java       |  4 +-
 .../src/main/java/accord/topology/Topologies.java  |  4 +-
 .../src/main/java/accord/topology/Topology.java    | 40 ++---------------
 .../main/java/accord/topology/TopologyManager.java |  4 +-
 .../src/main/java/accord/utils/ArrayBuffers.java   |  2 +-
 .../accord/utils/DeterministicIdentitySet.java     |  2 +-
 .../main/java/accord/utils/IndexedTriConsumer.java |  1 -
 .../java/accord/utils/IntrusiveLinkedList.java     |  2 +-
 .../java/accord/utils/IntrusiveLinkedListNode.java |  2 +-
 .../src/main/java/accord/utils/Invariants.java     |  5 +++
 .../src/main/java/accord/utils/MapReduce.java      |  2 +-
 .../src/main/java/accord/utils/SortedArrays.java   | 10 ++---
 accord-core/src/main/java/accord/utils/Utils.java  |  2 +-
 accord-core/src/test/java/accord/KeysTest.java     |  2 -
 .../accord/burn/BurnTestConfigurationService.java  |  2 +-
 .../src/test/java/accord/burn/TopologyUpdates.java |  2 +-
 .../tracking/InvalidationTrackerReconciler.java    |  3 +-
 .../tracking/RecoveryTrackerReconciler.java        |  2 +-
 .../coordinate/tracking/TrackerReconciler.java     |  4 +-
 .../src/test/java/accord/impl/TopologyFactory.java |  1 -
 .../src/test/java/accord/impl/basic/Cluster.java   |  7 +--
 .../src/test/java/accord/impl/list/ListAgent.java  |  2 +-
 .../test/java/accord/impl/list/ListRequest.java    |  2 +-
 .../test/java/accord/impl/mock/MockCluster.java    |  2 +-
 .../src/test/java/accord/impl/mock/Network.java    |  4 +-
 .../java/accord/topology/TopologyRandomizer.java   |  4 +-
 accord-core/src/test/java/accord/txn/DepsTest.java | 11 +----
 .../accord/verify/LinearizabilityVerifier.java     |  2 +-
 .../accord/verify/SerializabilityVerifier.java     |  7 +--
 .../accord/verify/SerializabilityVerifierTest.java |  2 +-
 .../verify/StrictSerializabilityVerifier.java      | 19 ++++----
 .../src/main/java/accord/maelstrom/Cluster.java    |  2 +-
 .../src/main/java/accord/maelstrom/Json.java       |  1 -
 .../java/accord/maelstrom/MaelstromRequest.java    |  1 -
 .../src/main/java/accord/maelstrom/Value.java      |  1 -
 .../src/test/java/accord/maelstrom/Runner.java     |  2 +-
 84 files changed, 229 insertions(+), 364 deletions(-)

diff --git a/accord-core/src/main/java/accord/api/Result.java b/accord-core/src/main/java/accord/api/Result.java
index be7abff..a5ac298 100644
--- a/accord-core/src/main/java/accord/api/Result.java
+++ b/accord-core/src/main/java/accord/api/Result.java
@@ -24,7 +24,7 @@ import accord.primitives.ProgressToken;
 /**
  * A result to be returned to a client, or be stored in a node's command state.
  *
- * TODO: support minimizing the result for storage in a node's command state (e.g. to only retain success/failure)
+ * TODO (expected, efficiency): support minimizing the result for storage in a node's command state (e.g. to only retain success/failure)
  */
 public interface Result extends Outcome
 {
diff --git a/accord-core/src/main/java/accord/api/Write.java b/accord-core/src/main/java/accord/api/Write.java
index 15fa6c8..6e5f439 100644
--- a/accord-core/src/main/java/accord/api/Write.java
+++ b/accord-core/src/main/java/accord/api/Write.java
@@ -25,7 +25,7 @@ import org.apache.cassandra.utils.concurrent.Future;
 /**
  * A collection of data to write to one or more stores
  *
- * TODO: support splitting so as to minimise duplication of data across shards
+ * TODO (desired, efficiency): support splitting so as to minimise duplication of data across shards
  */
 public interface Write
 {
diff --git a/accord-core/src/main/java/accord/coordinate/CheckOn.java b/accord-core/src/main/java/accord/coordinate/CheckOn.java
index 18ab6f9..d37a827 100644
--- a/accord-core/src/main/java/accord/coordinate/CheckOn.java
+++ b/accord-core/src/main/java/accord/coordinate/CheckOn.java
@@ -64,7 +64,7 @@ public class CheckOn extends CheckShards
 
     CheckOn(Node node, Known sufficient, TxnId txnId, Route<?> route, Unseekables<?, ?> routeWithHomeKey, long srcEpoch, long untilLocalEpoch, BiConsumer<? super CheckStatusOkFull, Throwable> callback)
     {
-        // TODO (soon): restore behaviour of only collecting info if e.g. Committed or Executed
+        // TODO (desired, efficiency): restore behaviour of only collecting info if e.g. Committed or Executed
         super(node, txnId, routeWithHomeKey, srcEpoch, IncludeInfo.All);
         Preconditions.checkArgument(routeWithHomeKey.contains(route.homeKey()));
         this.sufficient = sufficient;
@@ -73,7 +73,7 @@ public class CheckOn extends CheckShards
         this.untilLocalEpoch = untilLocalEpoch;
     }
 
-    // TODO: many callers only need to consult precisely executeAt.epoch remotely
+    // TODO (required, consider): many callers only need to consult precisely executeAt.epoch remotely
     public static CheckOn checkOn(Known sufficientStatus, Node node, TxnId txnId, Route<?> route, long srcEpoch, long untilLocalEpoch, BiConsumer<? super CheckStatusOkFull, Throwable> callback)
     {
         CheckOn checkOn = new CheckOn(node, sufficientStatus, txnId, route, srcEpoch, untilLocalEpoch, callback);
diff --git a/accord-core/src/main/java/accord/coordinate/CheckShards.java b/accord-core/src/main/java/accord/coordinate/CheckShards.java
index dd91993..9ad05fd 100644
--- a/accord-core/src/main/java/accord/coordinate/CheckShards.java
+++ b/accord-core/src/main/java/accord/coordinate/CheckShards.java
@@ -19,7 +19,7 @@ public abstract class CheckShards extends ReadCoordinator<CheckStatusReply>
 
     /**
      * The epoch until which we want to fetch data from remotely
-     * TODO: configure the epoch we want to start with
+     * TODO (required, consider): configure the epoch we want to start with
      */
     final long untilRemoteEpoch;
     final IncludeInfo includeInfo;
diff --git a/accord-core/src/main/java/accord/coordinate/Coordinate.java b/accord-core/src/main/java/accord/coordinate/Coordinate.java
index 836bfdb..ae65014 100644
--- a/accord-core/src/main/java/accord/coordinate/Coordinate.java
+++ b/accord-core/src/main/java/accord/coordinate/Coordinate.java
@@ -44,7 +44,7 @@ import static accord.messages.Commit.Invalidate.commitInvalidate;
  * Perform initial rounds of PreAccept and Accept until we have reached agreement about when we should execute.
  * If we are preempted by a recovery coordinator, we abort and let them complete (and notify us about the execution result)
  *
- * TODO: dedicated burn test to validate outcomes
+ * TODO (desired, testing): dedicated burn test to validate outcomes
  */
 public class Coordinate extends AsyncFuture<Result> implements Callback<PreAcceptReply>, BiConsumer<Result, Throwable>
 {
@@ -70,7 +70,7 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
 
     private void start()
     {
-        // TODO: consider sending only to electorate of most recent topology (as only these PreAccept votes matter)
+        // TODO (desired, efficiency): consider sending only to electorate of most recent topology (as only these PreAccept votes matter)
         // note that we must send to all replicas of old topology, as electorate may not be reachable
         node.send(tracker.nodes(), to -> new PreAccept(to, tracker.topologies(), txnId, txn, route), this);
     }
@@ -124,9 +124,9 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
         successes.add(ok);
 
         boolean fastPath = ok.witnessedAt.compareTo(txnId) == 0;
-        // TODO: update formalisation (and proof), as we do not seek additional pre-accepts from later epochs.
-        //       instead we rely on accept to do our work: a quorum of accept in the later epoch
-        //       and its effect on preaccepted timestamps and the deps it returns create our sync point.
+        // TODO (desired, safety): update formalisation (and proof), as we do not seek additional pre-accepts from later epochs.
+        //                         instead we rely on accept to do our work: a quorum of accept in the later epoch
+        //                         and its effect on preaccepted timestamps and the deps it returns create our sync point.
         if (tracker.recordSuccess(from, fastPath) == RequestStatus.Success)
             onPreAccepted();
     }
@@ -151,9 +151,10 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
                 executeAt = accumulate;
             }
 
-            // TODO: perhaps don't submit Accept immediately if we almost have enough for fast-path,
-            //       but by sending accept we rule out hybrid fast-path
-            // TODO: if we receive a MAX response, perhaps defer to permit at least one other node to respond before invalidating
+            // TODO (low priority, efficiency): perhaps don't submit Accept immediately if we almost have enough for fast-path,
+            //                                  but by sending accept we rule out hybrid fast-path
+            // TODO (low priority, efficiency): if we receive an expired response, perhaps defer to permit at least one other
+            //                                  node to respond before invalidating
             if (node.agent().isExpired(txnId, executeAt.real))
             {
                 proposeInvalidate(node, Ballot.ZERO, txnId, route.homeKey(), (success, fail) -> {
@@ -165,7 +166,7 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
                     {
                         node.withEpoch(executeAt.epoch, () -> {
                             commitInvalidate(node, txnId, route, executeAt);
-                            // TODO: this should be Invalidated rather than Timeout?
+                            // TODO (required, API consistency): this should be Invalidated rather than Timeout?
                             accept(null, new Timeout(txnId, route.homeKey()));
                         });
                     }
diff --git a/accord-core/src/main/java/accord/coordinate/FetchData.java b/accord-core/src/main/java/accord/coordinate/FetchData.java
index f1e876c..1bdba70 100644
--- a/accord-core/src/main/java/accord/coordinate/FetchData.java
+++ b/accord-core/src/main/java/accord/coordinate/FetchData.java
@@ -18,7 +18,7 @@ import static accord.local.Status.Outcome.OutcomeUnknown;
 /**
  * Find data and persist locally
  *
- * TODO accept lower bound epoch to avoid fetching data we should already have
+ * TODO (desired, efficiency): accept lower bound epoch to avoid fetching data we should already have
  */
 public class FetchData
 {
diff --git a/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java b/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
index 9ab76fc..79fac8b 100644
--- a/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
+++ b/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
@@ -69,7 +69,7 @@ public class InformHomeOfTxn extends AsyncFuture<Void> implements Callback<Simpl
                 break;
 
             case Nack:
-                // TODO: stale topology should be impossible right now
+                // TODO (required, consider): stale topology should be impossible right now
                 onFailure(from, new StaleTopology());
         }
     }
@@ -80,7 +80,7 @@ public class InformHomeOfTxn extends AsyncFuture<Void> implements Callback<Simpl
         if (this.failure == null) this.failure = failure;
         else this.failure.addSuppressed(failure);
 
-        // TODO: if we fail and have an incorrect topology, trigger refresh
+        // TODO (required, consider): if we fail and have an incorrect topology, trigger refresh
         if (tracker.onFailure(null) == Fail)
             tryFailure(this.failure);
     }
diff --git a/accord-core/src/main/java/accord/coordinate/Invalidate.java b/accord-core/src/main/java/accord/coordinate/Invalidate.java
index f923fe2..1ed1b75 100644
--- a/accord-core/src/main/java/accord/coordinate/Invalidate.java
+++ b/accord-core/src/main/java/accord/coordinate/Invalidate.java
@@ -166,7 +166,7 @@ public class Invalidate implements Callback<InvalidateReply>
                 case ReadyToExecute:
                 case PreApplied:
                 case Applied:
-                    // TODO: if we see Committed or above, go straight to Execute if we have assembled enough information
+                    // TODO (desired, efficiency): if we see Committed or above, go straight to Execute if we have assembled enough information
                     if (route != null)
                     {
                         // The data we see might have made it only to a minority in the event of PreAccept ONLY.
@@ -218,7 +218,7 @@ public class Invalidate implements Callback<InvalidateReply>
                     return;
 
                 case Invalidated:
-                    // TODO: standardise semantics of async/sync local application prior to callback
+                    // TODO (desired, API consistency): standardise semantics of whether local application of state prior is async or sync to callback
                     isDone = true;
                     commitInvalidate();
                     return;
@@ -227,13 +227,13 @@ public class Invalidate implements Callback<InvalidateReply>
 
         // if we have witnessed the transaction, but are able to invalidate, do we want to proceed?
         // Probably simplest to do so, but perhaps better for user if we don't.
-        // TODO (RangeTxns): This should be a Routable, or we should guarantee it is safe to operate on any key in the range
+        // TODO (now, rangetxns): This should be a Routable, or we should guarantee it is safe to operate on any key in the range
         RoutingKey invalidateWithKey = invalidateWith.slice(Ranges.of(tracker.promisedShard().range)).get(0).someIntersectingRoutingKey();
         proposeInvalidate(node, ballot, txnId, invalidateWithKey, (success, fail) -> {
-            /**
-             * We're now inside our *exactly once* callback we registered with proposeInvalidate, and we need to
-             * make sure we honour our own exactly once semantics with {@code callback}.
-             * So we are responsible for all exception handling.
+            /*
+              We're now inside our *exactly once* callback we registered with proposeInvalidate, and we need to
+              make sure we honour our own exactly once semantics with {@code callback}.
+              So we are responsible for all exception handling.
              */
             isDone = true;
             if (fail != null)
@@ -257,15 +257,15 @@ public class Invalidate implements Callback<InvalidateReply>
     private void commitInvalidate()
     {
         @Nullable Route<?> route = InvalidateReply.mergeRoutes(replies);
-        // TODO: commitInvalidate (and others) should skip the network for local applications,
+        // TODO (desired, efficiency): commitInvalidate (and others) should skip the network for local applications,
         //  so we do not need to explicitly do so here before notifying the waiter
         Commit.Invalidate.commitInvalidate(node, txnId, route != null ? Unseekables.merge(route, (Unseekables)invalidateWith) : invalidateWith, txnId);
-        // TODO: pick a reasonable upper bound, so we don't invalidate into an epoch/commandStore that no longer cares about this command
+        // TODO (required, consider): pick a reasonable upper bound, so we don't invalidate into an epoch/commandStore that no longer cares about this command
         node.forEachLocalSince(contextFor(txnId), invalidateWith, txnId, safeStore -> {
             safeStore.command(txnId).commitInvalidate(safeStore);
         }).addCallback((s, f) -> {
             callback.accept(INVALIDATED, null);
-            if (f != null) // TODO: consider exception handling more carefully: should we catch these prior to passing to callbacks?
+            if (f != null) // TODO (required): consider exception handling more carefully: should we catch these prior to passing to callbacks?
                 node.agent().onUncaughtException(f);
         });
     }
diff --git a/accord-core/src/main/java/accord/coordinate/MaybeRecover.java b/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
index 7840666..1522ec4 100644
--- a/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
+++ b/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
@@ -69,7 +69,7 @@ public class MaybeRecover extends CheckShards
 
     public boolean hasMadeProgress(CheckStatusOk ok)
     {
-        return ok != null && (ok.isCoordinating // TODO (now): make this coordinatingSince so can be pre-empted by others if stuck
+        return ok != null && (ok.isCoordinating // TODO (required, liveness): make this coordinatingSince so can be pre-empted by others if stuck
                               || ok.toProgressToken().compareTo(prevProgress) > 0);
     }
 
@@ -106,7 +106,7 @@ public class MaybeRecover extends CheckShards
                     break;
 
                 case InvalidationApplied:
-                    // TODO: we should simply invoke commitInvalidate
+                    // TODO (easy, efficiency): we should simply invoke commitInvalidate
                     Unseekables<?, ?> someKeys = reduceNonNull(Unseekables::merge, (Unseekables)contact, merged.route, route);
                     Invalidate.invalidate(node, txnId, someKeys.with(homeKey), callback);
             }
diff --git a/accord-core/src/main/java/accord/coordinate/Persist.java b/accord-core/src/main/java/accord/coordinate/Persist.java
index 7d9c997..a7daec9 100644
--- a/accord-core/src/main/java/accord/coordinate/Persist.java
+++ b/accord-core/src/main/java/accord/coordinate/Persist.java
@@ -92,7 +92,7 @@ public class Persist implements Callback<ApplyReply>
                 {
                     if (!isDone)
                     {
-                        // TODO: send to non-home replicas also, so they may clear their log more easily?
+                        // TODO (low priority, consider, efficiency): send to non-home replicas also, so they may clear their log more easily?
                         Shard homeShard = node.topology().forEpochIfKnown(route.homeKey(), txnId.epoch);
                         node.send(homeShard, new InformHomeDurable(txnId, route.homeKey(), executeAt, Durable, persistedOn));
                         isDone = true;
@@ -106,7 +106,7 @@ public class Persist implements Callback<ApplyReply>
                 break;
             case Insufficient:
                 Topologies topologies = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
-                // TODO (review with Ariel): use static method in Commit
+                // TODO (easy, cleanup): use static method in Commit
                 node.send(from, new Commit(Kind.Maximal, from, topologies.forEpoch(txnId.epoch), topologies, txnId, txn, route, null, executeAt, deps, false));
         }
     }
@@ -114,7 +114,7 @@ public class Persist implements Callback<ApplyReply>
     @Override
     public void onFailure(Id from, Throwable failure)
     {
-        // TODO: send knowledge of partial persistence?
+        // TODO (desired, consider): send knowledge of partial persistence?
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/coordinate/ReadCoordinator.java b/accord-core/src/main/java/accord/coordinate/ReadCoordinator.java
index 3c9ccbf..837b2cc 100644
--- a/accord-core/src/main/java/accord/coordinate/ReadCoordinator.java
+++ b/accord-core/src/main/java/accord/coordinate/ReadCoordinator.java
@@ -179,10 +179,10 @@ abstract class ReadCoordinator<Reply extends accord.messages.Reply> extends Read
     @Override
     protected RequestStatus trySendMore()
     {
-        // TODO: due to potential re-entrancy into this method, if the node we are contacting is unavailable
-        //  so onFailure is invoked immediately, for the moment we copy nodes to an intermediate list.
-        //  would be better to prevent reentrancy either by detecting this inside trySendMore or else queueing
-        //  callbacks externally, so two may not be in-flight at once
+        // TODO (low priority): due to potential re-entrancy into this method, if the node we are contacting is unavailable
+        //                      so onFailure is invoked immediately, for the moment we copy nodes to an intermediate list.
+        //                      would be better to prevent reentrancy either by detecting this inside trySendMore or else
+        //                      queueing callbacks externally, so two may not be in-flight at once
         List<Id> contact = new ArrayList<>(1);
         RequestStatus status = trySendMore(List::add, contact);
         contact.forEach(this::contact);
diff --git a/accord-core/src/main/java/accord/coordinate/Recover.java b/accord-core/src/main/java/accord/coordinate/Recover.java
index 12eee58..9f8e80c 100644
--- a/accord-core/src/main/java/accord/coordinate/Recover.java
+++ b/accord-core/src/main/java/accord/coordinate/Recover.java
@@ -51,14 +51,14 @@ import static accord.coordinate.tracking.RequestStatus.Failed;
 import static accord.coordinate.tracking.RequestStatus.Success;
 import static accord.messages.BeginRecovery.RecoverOk.maxAcceptedOrLater;
 
-// TODO: rename to Recover (verb); rename Recover message to not clash
+// TODO (low priority, cleanup): rename to Recover (verb); rename Recover message to not clash
 public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throwable>
 {
     class AwaitCommit extends AsyncFuture<Timestamp> implements Callback<WaitOnCommitOk>
     {
-        // TODO: this should collect the executeAt of any commit, and terminate as soon as one is found
-        //       that is earlier than TxnId for the Txn we are recovering; if all commits we wait for
-        //       are given earlier timestamps we can retry without restarting.
+        // TODO (desired, efficiency): this should collect the executeAt of any commit, and terminate as soon as one is found
+        //                             that is earlier than TxnId for the Txn we are recovering; if all commits we wait for
+        //                             are given earlier timestamps we can retry without restarting.
         final QuorumTracker tracker;
 
         AwaitCommit(Node node, TxnId txnId, Unseekables<?, ?> unseekables)
@@ -100,7 +100,6 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
         for (int i = 0 ; i < waitOn.txnIdCount() ; ++i)
         {
             TxnId txnId = waitOn.txnId(i);
-            // TODO (now): this should perhaps use RouteFragment as we might need to handle txns that are range-only
             new AwaitCommit(node, txnId, waitOn.someRoutables(txnId)).addCallback((success, failure) -> {
                 if (future.isDone())
                     return;
@@ -212,7 +211,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
 
                 case Applied:
                 case PreApplied:
-                    // TODO: in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
+                    // TODO (desired, efficiency): in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
                     node.withEpoch(executeAt.epoch, () -> {
                         CollectDeps.withDeps(node, txnId, route, txn, acceptOrCommit.executeAt, (deps, fail) -> {
                             if (fail != null)
@@ -221,7 +220,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
                             }
                             else
                             {
-                                // TODO: when writes/result are partially replicated, need to confirm we have quorum of these
+                                // TODO (required, consider): when writes/result are partially replicated, need to confirm we have quorum of these
                                 Persist.persistAndCommit(node, txnId, route, txn, executeAt, deps, acceptOrCommit.writes, acceptOrCommit.result);
                                 accept(acceptOrCommit.result, null);
                             }
@@ -232,7 +231,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
                 case ReadyToExecute:
                 case PreCommitted:
                 case Committed:
-                    // TODO: in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
+                    // TODO (desired, efficiency): in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
                     node.withEpoch(executeAt.epoch, () -> {
                         CollectDeps.withDeps(node, txnId, route, txn, executeAt, (deps, fail) -> {
                             if (fail != null) accept(null, fail);
@@ -273,7 +272,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
             // we have to be certain these commands have not successfully committed without witnessing us (thereby
             // ruling out a fast path decision for us and changing our recovery decision).
             // So, we wait for these commands to finish committing before retrying recovery.
-            // TODO: check paper: do we assume that witnessing in PreAccept implies witnessing in Accept? Not guaranteed.
+            // TODO (required): check paper: do we assume that witnessing in PreAccept implies witnessing in Accept? Not guaranteed.
             // See whitepaper for more details
             awaitCommits(node, earlierAcceptedNoWitness).addCallback((success, failure) -> {
                 if (failure != null) accept(null, failure);
diff --git a/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java b/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
index 59c95a1..775f1d7 100644
--- a/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
+++ b/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
@@ -136,7 +136,7 @@ public class RecoverWithRoute extends CheckShards
             case OutcomeKnown:
                 Invariants.checkState(known.definition.isKnown());
                 Invariants.checkState(known.executeAt.isDecisionKnown());
-                // TODO: we might not be able to reconstitute Txn if we have GC'd on some shards
+                // TODO (required): we might not be able to reconstitute Txn if we have GC'd on some shards
                 Txn txn = merged.partialTxn.reconstitute(route);
                 if (known.deps.isDecisionKnown())
                 {
diff --git a/accord-core/src/main/java/accord/coordinate/tracking/ReadTracker.java b/accord-core/src/main/java/accord/coordinate/tracking/ReadTracker.java
index e86f2d4..73d70f2 100644
--- a/accord-core/src/main/java/accord/coordinate/tracking/ReadTracker.java
+++ b/accord-core/src/main/java/accord/coordinate/tracking/ReadTracker.java
@@ -157,16 +157,16 @@ public class ReadTracker extends AbstractTracker<ReadTracker.ReadShardTracker, B
         }
     }
 
-    // TODO: abstract the candidate selection process so the implementation may prioritise based on distance/health etc
-    final Set<Id> inflight;    // TODO: use Agrona's IntHashSet as soon as Node.Id switches from long to int
-    final List<Id> candidates; // TODO: use Agrona's IntArrayList as soon as Node.Id switches from long to int
-    private Set<Id> slow;      // TODO: use Agrona's IntHashSet as soon as Node.Id switches from long to int
+    // TODO (required): abstract the candidate selection process so the implementation may prioritise based on distance/health etc
+    final Set<Id> inflight;    // TODO (easy, efficiency): use Agrona's IntHashSet as soon as Node.Id switches from long to int
+    final List<Id> candidates; // TODO (easy, efficiency): use Agrona's IntArrayList as soon as Node.Id switches from long to int
+    private Set<Id> slow;      // TODO (easy, efficiency): use Agrona's IntHashSet as soon as Node.Id switches from long to int
     protected int waitingOnData;
 
     public ReadTracker(Topologies topologies)
     {
         super(topologies, ReadShardTracker[]::new, ReadShardTracker::new);
-        this.candidates = new ArrayList<>(topologies.nodes()); // TODO: copyOfNodesAsList to avoid unnecessary copies
+        this.candidates = new ArrayList<>(topologies.nodes()); // TODO (low priority, efficiency): copyOfNodesAsList to avoid unnecessary copies
         this.inflight = newHashSetWithExpectedSize(maxShardsPerEpoch());
         this.waitingOnData = waitingOnShards;
     }
@@ -256,7 +256,7 @@ public class ReadTracker extends AbstractTracker<ReadTracker.ReadShardTracker, B
 
         Invariants.checkState(toRead != null, "We were asked to read more, but found no shards in need of reading more");
 
-        // TODO: maybe for each additional candidate do one linear compare run to find better secondary match
+        // TODO (desired, consider): maybe for each additional candidate do one linear compare run to find better secondary match
         //       OR at least discount candidates that do not contribute additional knowledge beyond those additional
         //       candidates already contacted, since implementations are likely to sort primarily by health
         candidates.sort((a, b) -> topologies().compare(a, b, toRead));
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
index f6b2d57..f112d0c 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
@@ -54,6 +54,7 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+// TODO (low priority): efficiency
 public class InMemoryCommandStore
 {
     public static abstract class State implements SafeCommandStore
@@ -84,7 +85,7 @@ public class InMemoryCommandStore
             return commands.get(txnId);
         }
 
-        // TODO (soon): mimic caching to test C* behaviour
+        // TODO (required): mimic caching to test C* behaviour
         public Command ifLoaded(TxnId txnId)
         {
             return commands.get(txnId);
@@ -227,7 +228,6 @@ public class InMemoryCommandStore
                 default:
                     throw new AssertionError();
                 case Key:
-                    // TODO: efficiency
                     AbstractKeys<Key, ?> keys = (AbstractKeys<Key, ?>) keysOrRanges;
                     return keys.stream()
                             .filter(slice::contains)
@@ -235,7 +235,6 @@ public class InMemoryCommandStore
                             .map(map)
                             .reduce(initialValue, reduce);
                 case Range:
-                    // TODO: efficiency
                     Ranges ranges = (Ranges) keysOrRanges;
                     return ranges.slice(slice).stream().flatMap(range ->
                             commandsForKey.subMap(range.start(), range.startInclusive(), range.end(), range.endInclusive()).values().stream()
@@ -254,7 +253,6 @@ public class InMemoryCommandStore
                     break;
                 case Range:
                     Ranges ranges = (Ranges) keysOrRanges;
-                    // TODO: zero allocation
                     ranges.slice(slice).forEach(range -> {
                         commandsForKey.subMap(range.start(), range.startInclusive(), range.end(), range.endInclusive())
                                 .values().forEach(forEach);
@@ -274,7 +272,6 @@ public class InMemoryCommandStore
                     break;
                 case Range:
                     Range range = (Range) keyOrRange;
-                    // TODO: zero allocation
                     Ranges.of(range).slice(slice).forEach(r -> {
                         commandsForKey.subMap(r.start(), r.startInclusive(), r.end(), r.endInclusive())
                                 .values().forEach(forEach);
diff --git a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
index c6069dc..060af15 100644
--- a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
+++ b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
@@ -63,7 +63,7 @@ import static accord.local.Status.PreApplied;
 import static accord.local.Status.PreCommitted;
 import static accord.primitives.Route.isFullRoute;
 
-// TODO: consider propagating invalidations in the same way as we do applied
+// TODO (desired, consider): consider propagating invalidations in the same way as we do applied
 public class SimpleProgressLog implements ProgressLog.Factory
 {
     enum Progress { NoneExpected, Expected, NoProgress, Investigating, Done }
@@ -184,7 +184,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
 
                 void updateMax(ProgressToken ok)
                 {
-                    // TODO: perhaps set localProgress back to Waiting if Investigating and we update anything?
+                    // TODO (low priority): perhaps set localProgress back to Waiting if Investigating and we update anything?
                     token = token.merge(ok);
                 }
 
@@ -247,7 +247,6 @@ public class SimpleProgressLog implements ProgressLog.Factory
                                                     return;
 
                                                 ProgressToken token = success.asProgressToken();
-                                                // TODO: avoid returning null (need to change semantics here in this case, though, as Recover doesn't return CheckStatusOk)
                                                 if (token.durability.isDurable())
                                                 {
                                                     commandStore.execute(contextFor(txnId), safeStore -> {
@@ -284,7 +283,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     @Override
                     public void onSuccess(Id from, SimpleReply reply)
                     {
-                        // TODO: callbacks should be associated with a commandStore for processing to avoid this
+                        // TODO (required, efficiency): callbacks should be associated with a commandStore for processing to avoid this
                         commandStore.execute(PreLoadContext.empty(), ignore -> {
                             if (progress() == Done)
                                 return;
@@ -306,8 +305,8 @@ public class SimpleProgressLog implements ProgressLog.Factory
                 }
 
                 DisseminateStatus status = NotExecuted;
-                Set<Id> notAwareOfDurability; // TODO: use Agrona's IntHashSet as soon as Node.Id switches from long to int
-                Set<Id> notPersisted;         // TODO: use Agrona's IntHashSet as soon as Node.Id switches from long to int
+                Set<Id> notAwareOfDurability; // TODO (easy, efficiency): use Agrona's IntHashSet as soon as Node.Id switches from long to int
+                Set<Id> notPersisted;         // TODO (easy, efficiency): use Agrona's IntHashSet as soon as Node.Id switches from long to int
 
                 List<Runnable> whenReady;
 
@@ -424,7 +423,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     setProgress(Investigating);
                     if (notAwareOfDurability.isEmpty())
                     {
-                        // TODO: also track actual durability
+                        // TODO (required, consider): also track actual durability
                         status = DisseminateStatus.Done;
                         setProgress(Done);
                         return;
@@ -483,7 +482,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     // first make sure we have enough information to obtain the command locally
                     Timestamp executeAt = command.hasBeen(PreCommitted) ? command.executeAt() : null;
                     long srcEpoch = (executeAt != null ? executeAt : txnId).epoch;
-                    // TODO: compute fromEpoch, the epoch we already have this txn replicated until
+                    // TODO (desired, consider): compute fromEpoch, the epoch we already have this txn replicated until
                     long toEpoch = Math.max(srcEpoch, node.topology().epoch());
                     Unseekables<?, ?> someKeys = unseekables(command);
 
@@ -514,7 +513,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                 private void invalidate(Node node, TxnId txnId, Unseekables<?, ?> someKeys)
                 {
                     setProgress(Investigating);
-                    // TODO (RangeTxns): This should be a Routable, or we should guarantee it is safe to operate on any key in the range
+                    // TODO (now, rangetxns): This should be a Routable, or we should guarantee it is safe to operate on any key in the range
                     RoutingKey someKey = Route.isRoute(someKeys) ? (Route.castToRoute(someKeys)).homeKey() : someKeys.get(0).someIntersectingRoutingKey();
                     someKeys = someKeys.with(someKey);
                     debugInvestigating = Invalidate.invalidate(node, txnId, someKeys, (success, fail) -> {
@@ -775,20 +774,20 @@ public class SimpleProgressLog implements ProgressLog.Factory
         public void durable(TxnId txnId, Unseekables<?, ?> unseekables, ProgressShard shard)
         {
             State state = ensure(txnId);
-            // TODO (progress consider-prerelease): we can probably simplify things by requiring (empty) Apply messages to be sent also to the coordinating topology
+            // TODO (desirable, efficiency): we can probably simplify things by requiring (empty) Apply messages to be sent also to the coordinating topology
             state.recordBlocking(txnId, PreApplied.minKnown, unseekables);
         }
 
         @Override
         public void waiting(TxnId blockedBy, Known blockedUntil, Unseekables<?, ?> blockedOn)
         {
-            // TODO (perf+ consider-prerelease): consider triggering a preemption of existing coordinator (if any) in some circumstances;
-            //     today, an LWT can pre-empt more efficiently (i.e. instantly) a failed operation whereas Accord will
-            //     wait for some progress interval before taking over; there is probably some middle ground where we trigger
-            //     faster preemption once we're blocked on a transaction, while still offering some amount of time to complete.
-            // TODO (soon): forward to local progress shard for processing (if known)
-            // TODO (soon): if we are co-located with the home shard, don't need to do anything unless we're in a
-            //              later topology that wasn't covered by its coordination
+            // TODO (consider): consider triggering a preemption of existing coordinator (if any) in some circumstances;
+            //                  today, an LWT can pre-empt more efficiently (i.e. instantly) a failed operation whereas Accord will
+            //                  wait for some progress interval before taking over; there is probably some middle ground where we trigger
+            //                  faster preemption once we're blocked on a transaction, while still offering some amount of time to complete.
+            // TODO (desirable, efficiency): forward to local progress shard for processing (if known)
+            // TODO (desirable, efficiency): if we are co-located with the home shard, don't need to do anything unless we're in a
+            //                               later topology that wasn't covered by its coordination
             ensure(blockedBy).recordBlocking(blockedBy, blockedUntil, blockedOn);
         }
 
diff --git a/accord-core/src/main/java/accord/local/Command.java b/accord-core/src/main/java/accord/local/Command.java
index 592c90f..3915411 100644
--- a/accord-core/src/main/java/accord/local/Command.java
+++ b/accord-core/src/main/java/accord/local/Command.java
@@ -72,11 +72,10 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
     public abstract TxnId txnId();
 
-    // TODO (now): pack this into TxnId
     public abstract Kind kind();
     public abstract void setKind(Kind kind);
 
-    // TODO (now): should any of these calls be replaced by corresponding known() registers?
+    // TODO (desirable, API consistency): should any of these calls be replaced by corresponding known() registers?
     public boolean hasBeen(Status status)
     {
         return status().hasBeen(status);
@@ -120,9 +119,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
      * so that there is only one copy per node that can be consulted to construct the full set of involved keys.
      *
      * If hasBeen(Committed) this must contain the keys for both txnId.epoch and executeAt.epoch
-     *
-     * TODO: maybe set this for all local shards, but slice to only those participating keys
-     * (would probably need to remove hashIntersects)
      */
     public abstract @Nullable Route<?> route();
     protected abstract void setRoute(Route<?> route);
@@ -193,7 +189,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
     @Override
     public Seekables<?, ?> keys()
     {
-        // TODO (now): when do we need this, and will it always be sufficient?
+        // TODO (expected, consider): when do we need this, and will it always be sufficient?
         return partialTxn().keys();
     }
 
@@ -265,7 +261,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         }
         else
         {
-            // TODO: in the case that we are pre-committed but had not been preaccepted/accepted, should we inform progressLog?
+            // TODO (expected, ?): in the case that we are pre-committed but had not been preaccepted/accepted, should we inform progressLog?
             setSaveStatus(SaveStatus.enrich(saveStatus(), DefinitionOnly));
         }
         set(safeStore, Ranges.EMPTY, coordinateRanges, shard, route, partialTxn, Set, null, Ignore);
@@ -373,7 +369,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         }
 
         Ranges coordinateRanges = coordinateRanges(safeStore);
-        // TODO (now): consider ranges between coordinateRanges and executeRanges? Perhaps don't need them
+        // TODO (expected, consider): consider ranges between coordinateRanges and executeRanges? Perhaps don't need them
         Ranges executeRanges = executeRanges(safeStore, executeAt);
         ProgressShard shard = progressShard(safeStore, route, progressKey, coordinateRanges);
 
@@ -389,7 +385,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
         safeStore.progressLog().committed(this, shard);
 
-        // TODO (now): introduce intermediate status to avoid reentry when notifying listeners (which might notify us)
+        // TODO (expected, safety): introduce intermediate status to avoid reentry when notifying listeners (which might notify us)
         maybeExecute(safeStore, shard, true, true);
         return CommitOutcome.Success;
     }
@@ -438,9 +434,10 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
                             break;
                         case PreCommitted:
                         case Committed:
-                            // TODO: split into ReadyToRead and ReadyToWrite;
-                            //       the distributed read can be performed as soon as those keys are ready, and in parallel with any other reads
-                            //       the client can even ACK immediately after; only the write needs to be postponed until other in-progress reads complete
+                            // TODO (desired, efficiency): split into ReadyToRead and ReadyToWrite;
+                            //                             the distributed read can be performed as soon as those keys are ready,
+                            //                             and in parallel with any other reads. the client can even ACK immediately after;
+                            //                             only the write needs to be postponed until other in-progress reads complete
                         case ReadyToExecute:
                         case PreApplied:
                         case Applied:
@@ -454,7 +451,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         }
     }
 
-    // TODO (now): commitInvalidate may need to update cfks _if_ possible
+    // TODO (expected, ?): commitInvalidate may need to update cfks _if_ possible
     public void commitInvalidate(SafeCommandStore safeStore)
     {
         if (hasBeen(PreCommitted))
@@ -501,7 +498,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         ProgressShard shard = progressShard(safeStore, route, coordinateRanges);
 
         if (!validate(coordinateRanges, executeRanges, shard, route, Check, null, Check, partialDeps, hasBeen(Committed) ? Add : TrySet))
-            return ApplyOutcome.Insufficient; // TODO: this should probably be an assertion failure if !TrySet
+            return ApplyOutcome.Insufficient; // TODO (expected, consider): this should probably be an assertion failure if !TrySet
 
         setWrites(writes);
         setResult(result);
@@ -582,9 +579,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         return partialTxn().read(safeStore, this);
     }
 
-    // TODO: maybe split into maybeExecute and maybeApply?
-    // TODO (performance): If we are a no-op on this shard, just immediately apply.
-    //      NOTE: if we ever do transitive dependency elision this could be dangerous
+    // TODO (expected, API consistency): maybe split into maybeExecute and maybeApply?
     private boolean maybeExecute(SafeCommandStore safeStore, ProgressShard shard, boolean alwaysNotifyListeners, boolean notifyWaitingOn)
     {
         if (logger.isTraceEnabled())
@@ -610,7 +605,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         switch (status())
         {
             case Committed:
-                // TODO: maintain distinct ReadyToRead and ReadyToWrite states
                 setStatus(ReadyToExecute);
                 logger.trace("{}: set to ReadyToExecute", txnId());
                 safeStore.progressLog().readyToExecute(this, shard);
@@ -625,6 +619,8 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
                 }
                 else
                 {
+                    // TODO (desirable, performance): This could be performed immediately upon Committed
+                    //      but: if we later support transitive dependency elision this could be dangerous
                     logger.trace("{}: applying no-op", txnId());
                     setStatus(Applied);
                     notifyListeners(safeStore);
@@ -644,7 +640,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         {
             logger.trace("{}: {} is invalidated. Stop listening and removing from waiting on commit set.", txnId(), dependency.txnId());
             dependency.removeListener(this);
-            removeWaitingOnCommit(dependency.txnId()); // TODO (now): this was missing in partial-replication; might be redundant?
+            removeWaitingOnCommit(dependency.txnId());
             return true;
         }
         else if (dependency.executeAt().compareTo(executeAt()) > 0)
@@ -819,10 +815,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
      *
      * Note that for ProgressLog purposes the "home shard" is the shard as of txnId.epoch.
      * For recovery purposes the "home shard" is as of txnId.epoch until Committed, and executeAt.epoch once Executed
-     *
-     * TODO: Markdown documentation explaining the home shard and local shard concepts
      */
-
     public final void homeKey(RoutingKey homeKey)
     {
         RoutingKey current = homeKey();
@@ -1007,7 +1000,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (shard.isProgress()) setRoute(Route.merge(route(), (Route)route));
         else setRoute(Route.merge(route(), (Route)route.slice(allRanges)));
 
-        // TODO (soon): stop round-robin hashing; partition only on ranges
         switch (ensurePartialTxn)
         {
             case Add:
@@ -1018,7 +1010,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
                 {
                     partialTxn = partialTxn.slice(allRanges, shard.isHome());
                     Routables.foldlMissing((Seekables)partialTxn.keys(), partialTxn().keys(), (keyOrRange, p, v, i) -> {
-                        // TODO: duplicate application of ranges
                         safeStore.forEach(keyOrRange, allRanges, forKey -> forKey.register(this));
                         return v;
                     }, 0, 0, 1);
@@ -1030,9 +1021,9 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
             case TrySet:
                 setKind(partialTxn.kind());
                 setPartialTxn(partialTxn = partialTxn.slice(allRanges, shard.isHome()));
-                // TODO: duplicate application of ranges
+                // TODO (expected, efficiency): we may register the same ranges more than once
                 safeStore.forEach(partialTxn.keys(), allRanges, forKey -> {
-                    // TODO: no need to register on PreAccept if already Accepted
+                    // TODO (desirable, efficiency): no need to register on PreAccept if already Accepted
                     forKey.register(this);
                 });
                 break;
@@ -1134,7 +1125,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         return true;
     }
 
-    // TODO: callers should try to consult the local progress shard (if any) to obtain the full set of keys owned locally
+    // TODO (low priority, progress): callers should try to consult the local progress shard (if any) to obtain the full set of keys owned locally
     public Route<?> someRoute()
     {
         if (route() != null)
@@ -1196,8 +1187,8 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         return txn != null && txn.query() != null;
     }
 
-    // TODO: this is an ugly hack, need to encode progress/homeKey/Route state combinations much more clearly
-    //  (perhaps introduce encapsulating class representing each possible arrangement)
+    // TODO (low priority, API): this is an ugly hack, need to encode progress/homeKey/Route state combinations much more clearly
+    //                           (perhaps introduce encapsulating class representing each possible arrangement)
     static class NoProgressKey implements RoutingKey
     {
         @Override
@@ -1205,7 +1196,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         {
             throw new UnsupportedOperationException();
         }
-
     }
 
     private static final NoProgressKey NO_PROGRESS_KEY = new NoProgressKey();
diff --git a/accord-core/src/main/java/accord/local/CommandsForKey.java b/accord-core/src/main/java/accord/local/CommandsForKey.java
index eb03f28..142440f 100644
--- a/accord-core/src/main/java/accord/local/CommandsForKey.java
+++ b/accord-core/src/main/java/accord/local/CommandsForKey.java
@@ -62,11 +62,7 @@ public abstract class CommandsForKey implements CommandListener
          * Note that {@code testDep} applies only to commands that know at least proposed deps; if specified any
          * commands that do not know any deps will be ignored.
          *
-         * TODO (soon): TestDep should be asynchronous; data should not be kept memory-resident as only used for recovery
-         *
-         * TODO: we don't really need TestStatus anymore, but for clarity it might be nice to retain it to declare intent.
-         *       This is because we only use it in places where TestDep is specified, and the statuses we want to rule-out
-         *       do not have any deps.
+         * TODO (expected, efficiency): TestDep should be asynchronous; data should not be kept memory-resident as only used for recovery
          */
         Stream<T> before(Timestamp timestamp, TestKind testKind, TestDep testDep, @Nullable TxnId depId, TestStatus testStatus, @Nullable Status status);
 
diff --git a/accord-core/src/main/java/accord/local/Node.java b/accord-core/src/main/java/accord/local/Node.java
index e348a76..6b1ed4e 100644
--- a/accord-core/src/main/java/accord/local/Node.java
+++ b/accord-core/src/main/java/accord/local/Node.java
@@ -104,11 +104,6 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         return promised.node.equals(id) && coordinating.containsKey(txnId);
     }
 
-    public static int numCommandShards()
-    {
-        return 8; // TODO: make configurable
-    }
-
     private final Id id;
     private final MessageSink messageSink;
     private final ConfigurationService configService;
@@ -120,10 +115,10 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
     private final Agent agent;
     private final Random random;
 
-    // TODO: this really needs to be thought through some more, as it needs to be per-instance in some cases, and per-node in others
+    // TODO (expected, consider): this really needs to be thought through some more, as it needs to be per-instance in some cases, and per-node in others
     private final Scheduler scheduler;
 
-    // TODO (soon): monitor the contents of this collection for stalled coordination, and excise them
+    // TODO (expected, liveness): monitor the contents of this collection for stalled coordination, and excise them
     private final Map<TxnId, Future<? extends Outcome>> coordinating = new ConcurrentHashMap<>();
 
     public Node(Id id, MessageSink messageSink, ConfigurationService configService, LongSupplier nowSupplier,
@@ -228,7 +223,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
     public Timestamp uniqueNow()
     {
         return now.updateAndGet(cur -> {
-            // TODO: this diverges from proof; either show isomorphism or make consistent
+            // TODO (low priority, proof): this diverges from proof; either show isomorphism or make consistent
             long now = nowSupplier.getAsLong();
             long epoch = Math.max(cur.epoch, topology.epoch());
             return (now > cur.real)
@@ -370,12 +365,11 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public Future<Result> coordinate(TxnId txnId, Txn txn)
     {
-        // TODO: The combination of updating the epoch of the next timestamp with epochs we don't have topologies for,
+        // TODO (desirable, consider): The combination of updating the epoch of the next timestamp with epochs we don't have topologies for,
         //  and requiring preaccept to talk to its topology epoch means that learning of a new epoch via timestamp
         //  (ie not via config service) will halt any new txns from a node until it receives this topology
         Future<Result> result = withEpoch(txnId.epoch, () -> initiateCoordination(txnId, txn));
         coordinating.putIfAbsent(txnId, result);
-        // TODO: if we fail, nominate another node to try instead
         result.addCallback((success, fail) -> coordinating.remove(txnId, result));
         return result;
     }
@@ -470,14 +464,11 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
             return future;
         });
         coordinating.putIfAbsent(txnId, result);
-        result.addCallback((success, fail) -> {
-            coordinating.remove(txnId, result);
-            // TODO: if we fail, nominate another node to try instead
-        });
+        result.addCallback((success, fail) -> coordinating.remove(txnId, result));
         return result;
     }
 
-    // TODO: coalesce other maybeRecover calls also? perhaps have mutable knownStatuses so we can inject newer ones?
+    // TODO (low priority, API/efficiency): coalesce maybeRecover calls? perhaps have mutable knownStatuses so we can inject newer ones?
     public Future<? extends Outcome> maybeRecover(TxnId txnId, RoutingKey homeKey, @Nullable Route<?> route, ProgressToken prevProgress)
     {
         Future<? extends Outcome> result = coordinating.get(txnId);
diff --git a/accord-core/src/main/java/accord/local/PartialCommand.java b/accord-core/src/main/java/accord/local/PartialCommand.java
deleted file mode 100644
index 93756d8..0000000
--- a/accord-core/src/main/java/accord/local/PartialCommand.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package accord.local;
-
-import accord.primitives.*;
-
-/**
- * A minimal, read only view of a full command. Provided so C* can denormalize command data onto
- * CommandsForKey and Command implementations. Implementer must guarantee that PartialCommand
- * implementations behave as an immutable view and are always in sync with the 'real' command;
- */
-public interface PartialCommand
-{
-    TxnId txnId();
-    Timestamp executeAt();
-
-    // TODO: pack this into TxnId
-    Txn.Kind kind();
-    Status status();
-
-    default boolean hasBeen(Status status)
-    {
-        return status().hasBeen(status);
-    }
-    default boolean is(Status status)
-    {
-        return status() == status;
-    }
-}
diff --git a/accord-core/src/main/java/accord/local/PreLoadContext.java b/accord-core/src/main/java/accord/local/PreLoadContext.java
index 0c5b5ab..d00e1c4 100644
--- a/accord-core/src/main/java/accord/local/PreLoadContext.java
+++ b/accord-core/src/main/java/accord/local/PreLoadContext.java
@@ -58,7 +58,7 @@ public interface PreLoadContext
         switch (keysOrRanges.kindOfContents())
         {
             default: throw new AssertionError();
-            case Range: return contextFor(txnId); // TODO (soon): this won't work for actual range queries
+            case Range: return contextFor(txnId); // TODO (required, correctness): this won't work for actual range queries
             case Key: return contextFor(Collections.singleton(txnId), keysOrRanges);
         }
     }
diff --git a/accord-core/src/main/java/accord/local/SaveStatus.java b/accord-core/src/main/java/accord/local/SaveStatus.java
index caae672..3215d1a 100644
--- a/accord-core/src/main/java/accord/local/SaveStatus.java
+++ b/accord-core/src/main/java/accord/local/SaveStatus.java
@@ -53,7 +53,7 @@ public enum SaveStatus
     
     public final Status status;
     public final Phase phase;
-    public final Known known; // TODO: duplicate contents here to reduce indirection for majority of cases
+    public final Known known; // TODO (easy, API/efficiency): duplicate contents here to reduce indirection for majority of cases
 
     SaveStatus(Status status)
     {
@@ -74,7 +74,7 @@ public enum SaveStatus
         return this.status.compareTo(status) >= 0;
     }
 
-    // TODO: exhaustive testing, particularly around PreCommitted
+    // TODO (expected, testing): exhaustive testing, particularly around PreCommitted
     public static SaveStatus get(Status status, Known known)
     {
         switch (status)
@@ -88,7 +88,7 @@ public enum SaveStatus
                     return known.isDefinitionKnown() ? AcceptedInvalidateWithDefinition : AcceptedInvalidate;
                 // If we know the executeAt decision then we do not clear it, and fall-through to PreCommitted
                 // however, we still clear the deps, as any deps we might have previously seen proposed are now expired
-                // TODO: consider clearing Command.partialDeps in this case also
+                // TODO (expected, consider): consider clearing Command.partialDeps in this case also
                 known = known.with(DepsUnknown);
             case Accepted:
                 if (!known.executeAt.isDecisionKnown())
diff --git a/accord-core/src/main/java/accord/local/ShardDistributor.java b/accord-core/src/main/java/accord/local/ShardDistributor.java
index d834929..fd73446 100644
--- a/accord-core/src/main/java/accord/local/ShardDistributor.java
+++ b/accord-core/src/main/java/accord/local/ShardDistributor.java
@@ -11,8 +11,8 @@ import java.util.function.Function;
 
 public interface ShardDistributor
 {
-    // TODO: this is overly simplistic: need to supply existing distribution,
-    //  and support gradual local redistribution to keep number of shards eventually the same
+    // TODO (expected, topology): this is overly simplistic: need to supply existing distribution, and support
+    //                            gradual local redistribution to keep number of shards eventually the same
     List<Ranges> split(Ranges ranges);
 
     class EvenSplit<T> implements ShardDistributor
diff --git a/accord-core/src/main/java/accord/local/Status.java b/accord-core/src/main/java/accord/local/Status.java
index ad60e00..494bcc8 100644
--- a/accord-core/src/main/java/accord/local/Status.java
+++ b/accord-core/src/main/java/accord/local/Status.java
@@ -370,8 +370,9 @@ public enum Status
         this.minKnown = new Known(definition, executeAt, deps, outcome);
     }
 
-    // TODO: investigate all uses of hasBeen, and migrate as many as possible to testing Phase, ReplicationPhase and ExecutionStatus
-    //       where these concepts are inadequate, see if additional concepts can be introduced
+    // TODO (desired, clarity): investigate all uses of hasBeen, and migrate as many as possible to testing
+    //                          Phase, ReplicationPhase and ExecutionStatus where these concepts are inadequate,
+    //                          see if additional concepts can be introduced
     public boolean hasBeen(Status equalOrGreaterThan)
     {
         return compareTo(equalOrGreaterThan) >= 0;
diff --git a/accord-core/src/main/java/accord/local/SyncCommandStores.java b/accord-core/src/main/java/accord/local/SyncCommandStores.java
index fde0fe4..93476f7 100644
--- a/accord-core/src/main/java/accord/local/SyncCommandStores.java
+++ b/accord-core/src/main/java/accord/local/SyncCommandStores.java
@@ -10,7 +10,7 @@ import accord.utils.MapReduceConsume;
 import java.util.function.Function;
 import java.util.stream.IntStream;
 
-// TODO (soon): introduce new CommandStores that mimics asynchrony by integrating with Cluster scheduling for List workload
+// TODO (desired, testing): introduce new CommandStores that mimics asynchrony by integrating with Cluster scheduling for List workload
 public class SyncCommandStores extends CommandStores<SyncCommandStores.SyncCommandStore>
 {
     public interface SafeSyncCommandStore extends SafeCommandStore
diff --git a/accord-core/src/main/java/accord/messages/Accept.java b/accord-core/src/main/java/accord/messages/Accept.java
index 59dcc10..22d2a87 100644
--- a/accord-core/src/main/java/accord/messages/Accept.java
+++ b/accord-core/src/main/java/accord/messages/Accept.java
@@ -41,7 +41,8 @@ import javax.annotation.Nullable;
 import static accord.local.Command.AcceptOutcome.*;
 import static accord.messages.PreAccept.calculatePartialDeps;
 
-// TODO: use different objects for send and receive, so can be more efficient (e.g. serialize without slicing, and without unnecessary fields)
+// TODO (low priority, efficiency): use different objects for send and receive, so can be more efficient
+//                                  (e.g. serialize without slicing, and without unnecessary fields)
 public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
 {
     public static class SerializerSupport
@@ -90,7 +91,7 @@ public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
             case RejectedBallot:
                 return new AcceptReply(command.promised());
             case Success:
-                // TODO: we don't need to calculate deps if executeAt == txnId
+                // TODO (desirable, efficiency): we don't need to calculate deps if executeAt == txnId
                 return new AcceptReply(calculatePartialDeps(safeStore, txnId, keys, kind, executeAt, safeStore.ranges().between(minEpoch, executeAt.epoch)));
         }
     }
diff --git a/accord-core/src/main/java/accord/messages/Apply.java b/accord-core/src/main/java/accord/messages/Apply.java
index c1709ba..8554af2 100644
--- a/accord-core/src/main/java/accord/messages/Apply.java
+++ b/accord-core/src/main/java/accord/messages/Apply.java
@@ -53,7 +53,6 @@ public class Apply extends TxnRequest<ApplyReply>
     {
         super(to, sendTo, route, txnId);
         this.untilEpoch = untilEpoch;
-        // TODO: we shouldn't send deps unless we need to (but need to implement fetching them if they're not present)
         Ranges slice = applyTo == sendTo ? scope.covering() : applyTo.computeRangesForNode(to);
         this.deps = deps.slice(slice);
         this.executeAt = executeAt;
diff --git a/accord-core/src/main/java/accord/messages/BeginRecovery.java b/accord-core/src/main/java/accord/messages/BeginRecovery.java
index 8421c63..db8958c 100644
--- a/accord-core/src/main/java/accord/messages/BeginRecovery.java
+++ b/accord-core/src/main/java/accord/messages/BeginRecovery.java
@@ -123,7 +123,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
             if (!rejectsFastPath)
                 rejectsFastPath = committedExecutesAfterWithoutWitnessing(safeStore, txnId, ranges, partialTxn.keys()).anyMatch(ignore -> true);
 
-            // TODO: introduce some good unit tests for verifying these two functions in a real repair scenario
+            // TODO (expected, testing): introduce some good unit tests for verifying these two functions in a real repair scenario
             // committed txns with an earlier txnid and have our txnid as a dependency
             earlierCommittedWitness = committedStartedBeforeAndWitnessed(safeStore, txnId, ranges, partialTxn.keys());
 
@@ -136,8 +136,8 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
     @Override
     public RecoverReply reduce(RecoverReply r1, RecoverReply r2)
     {
-        // TODO: should not operate on dependencies directly here, as we only merge them;
-        //       should have a cheaply mergeable variant (or should collect them before merging)
+        // TODO (low priority, efficiency): should not operate on dependencies directly here, as we only merge them;
+        //                                  want a cheaply mergeable variant (or should collect them before merging)
 
         if (!r1.isOk()) return r1;
         if (!r2.isOk()) return r2;
@@ -213,7 +213,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
 
     public static class RecoverOk extends RecoverReply
     {
-        public final TxnId txnId; // TODO for debugging?
+        public final TxnId txnId; // for debugging
         public final Status status;
         public final Ballot accepted;
         public final Timestamp executeAt;
diff --git a/accord-core/src/main/java/accord/messages/Callback.java b/accord-core/src/main/java/accord/messages/Callback.java
index 932ad44..69a663f 100644
--- a/accord-core/src/main/java/accord/messages/Callback.java
+++ b/accord-core/src/main/java/accord/messages/Callback.java
@@ -22,7 +22,8 @@ import accord.local.Node.Id;
 
 /**
  * Represents some execution for handling responses from messages a node has sent.
- * TODO: associate a Callback with a CommandShard or other context for execution (for coordination, usually its home shard)
+ * TODO (expected, efficiency): associate a Callback with a CommandShard or other context for execution
+ *                              (for coordination, usually its home shard)
  */
 public interface Callback<T>
 {
diff --git a/accord-core/src/main/java/accord/messages/Commit.java b/accord-core/src/main/java/accord/messages/Commit.java
index 74521d0..1d9f47a 100644
--- a/accord-core/src/main/java/accord/messages/Commit.java
+++ b/accord-core/src/main/java/accord/messages/Commit.java
@@ -37,7 +37,6 @@ import accord.topology.Topology;
 import static accord.local.Status.Committed;
 import static accord.local.Status.Known.DefinitionOnly;
 
-// TODO: CommitOk responses, so we can send again if no reply received? Or leave to recovery?
 public class Commit extends TxnRequest<ReadNack>
 {
     public static class SerializerSupport
@@ -59,8 +58,8 @@ public class Commit extends TxnRequest<ReadNack>
 
     public enum Kind { Minimal, Maximal }
 
-    // TODO: cleanup passing of topologies here - maybe fetch them afresh from Node? Or perhaps introduce well-named
-    //       classes to represent different topology combinations
+    // TODO (low priority, clarity): cleanup passing of topologies here - maybe fetch them afresh from Node?
+    //                               Or perhaps introduce well-named classes to represent different topology combinations
     public Commit(Kind kind, Id to, Topology coordinateTopology, Topologies topologies, TxnId txnId, Txn txn, FullRoute<?> route, @Nullable Seekables<?, ?> readScope, Timestamp executeAt, Deps deps, boolean read)
     {
         super(to, topologies, route, txnId);
@@ -100,8 +99,8 @@ public class Commit extends TxnRequest<ReadNack>
         this.read = read;
     }
 
-    // TODO (soon): accept Topology not Topologies
-    // TODO: do not commit if we're already ready to execute (requires extra info in Accept responses)
+    // TODO (low priority, clarity): accept Topology not Topologies
+    // TODO (desired, efficiency): do not commit if we're already ready to execute (requires extra info in Accept responses)
     public static void commitMinimalAndRead(Node node, Topologies executeTopologies, TxnId txnId, Txn txn, FullRoute<?> route, Seekables<?, ?> readScope, Timestamp executeAt, Deps deps, Set<Id> readSet, Callback<ReadReply> callback)
     {
         Topologies allTopologies = executeTopologies;
@@ -144,7 +143,7 @@ public class Commit extends TxnRequest<ReadNack>
         node.mapReduceConsumeLocal(this, txnId.epoch, executeAt.epoch, this);
     }
 
-    // TODO (soon): do not guard with synchronized; let mapReduceLocal decide how to enforce mutual exclusivity
+    // TODO (expected, efficiency, clarity): do not guard with synchronized; let mapReduceLocal decide how to enforce mutual exclusivity
     @Override
     public synchronized ReadNack apply(SafeCommandStore safeStore)
     {
@@ -213,7 +212,7 @@ public class Commit extends TxnRequest<ReadNack>
 
         public static void commitInvalidate(Node node, TxnId txnId, Unseekables<?, ?> inform, long untilEpoch)
         {
-            // TODO: this kind of check needs to be inserted in all equivalent methods
+            // TODO (expected, safety): this kind of check needs to be inserted in all equivalent methods
             Invariants.checkState(untilEpoch >= txnId.epoch);
             Invariants.checkState(node.topology().hasEpoch(untilEpoch));
             Topologies commitTo = node.topology().preciseEpochs(inform, txnId.epoch, untilEpoch);
diff --git a/accord-core/src/main/java/accord/messages/Defer.java b/accord-core/src/main/java/accord/messages/Defer.java
index 07a592b..ede92ba 100644
--- a/accord-core/src/main/java/accord/messages/Defer.java
+++ b/accord-core/src/main/java/accord/messages/Defer.java
@@ -1,25 +1,24 @@
 package accord.messages;
 
-import java.util.BitSet;
 import java.util.function.Function;
 
 import accord.local.*;
 import accord.local.Status.Known;
 import accord.primitives.TxnId;
 import accord.utils.Invariants;
+import com.carrotsearch.hppc.IntHashSet;
 
 import static accord.messages.Defer.Ready.Expired;
 import static accord.messages.Defer.Ready.No;
 import static accord.messages.Defer.Ready.Yes;
 
-// TODO: use something more efficient? could probably assign each CommandStore a unique ascending integer and use an int[]
 class Defer implements CommandListener
 {
     public enum Ready { No, Yes, Expired }
 
     final Function<Command, Ready> waitUntil;
     final TxnRequest<?> request;
-    BitSet waitingOn = new BitSet(); // TODO: move to compressed integer hash map to permit easier reclamation of ids
+    IntHashSet waitingOn = new IntHashSet(); // TODO (easy): use Agrona when available
     int waitingOnCount;
     boolean isDone;
 
@@ -45,7 +44,7 @@ class Defer implements CommandListener
         if (isDone)
             throw new IllegalStateException("Recurrent retry of " + request);
 
-        waitingOn.set(commandStore.id());
+        waitingOn.add(commandStore.id());
         ++waitingOnCount;
         command.addListener(this);
     }
@@ -59,9 +58,9 @@ class Defer implements CommandListener
         if (ready == Expired) return;
 
         int id = safeStore.commandStore().id();
-        if (!waitingOn.get(id))
+        if (!waitingOn.contains(id))
             throw new IllegalStateException();
-        waitingOn.clear(id);
+        waitingOn.remove(id);
 
         if (0 == --waitingOnCount)
         {
diff --git a/accord-core/src/main/java/accord/messages/GetDeps.java b/accord-core/src/main/java/accord/messages/GetDeps.java
index fd62a91..b2f296d 100644
--- a/accord-core/src/main/java/accord/messages/GetDeps.java
+++ b/accord-core/src/main/java/accord/messages/GetDeps.java
@@ -50,7 +50,6 @@ public class GetDeps extends TxnRequest.WithUnsynced<PartialDeps>
     @Override
     public PartialDeps apply(SafeCommandStore instance)
     {
-        // TODO: shrink ranges to those that intersect key
         Ranges ranges = instance.ranges().between(minEpoch, executeAt.epoch);
         return calculatePartialDeps(instance, txnId, keys, kind, executeAt, ranges);
     }
diff --git a/accord-core/src/main/java/accord/messages/InformDurable.java b/accord-core/src/main/java/accord/messages/InformDurable.java
index 3631003..da6123a 100644
--- a/accord-core/src/main/java/accord/messages/InformDurable.java
+++ b/accord-core/src/main/java/accord/messages/InformDurable.java
@@ -52,8 +52,8 @@ public class InformDurable extends TxnRequest<Reply> implements PreLoadContext
         {
             // we need to pick a progress log, but this node might not have participated in the coordination epoch
             // in this rare circumstance we simply pick a key to select some progress log to coordinate this
-            // TODO (now): We might not replicate either txnId.epoch OR executeAt.epoch, but some inbetween.
-            //             Do we need to receive this message in that case? If so, we need to account for this when selecting a progress key
+            // TODO (required, consider): We might not replicate either txnId.epoch OR executeAt.epoch, but some inbetween.
+            //                            Do we need to receive this message in that case? If so, we need to account for this when selecting a progress key
             at = executeAt;
             progressKey = node.selectProgressKey(executeAt.epoch, scope, scope.homeKey());
             shard = Adhoc;
@@ -63,7 +63,7 @@ public class InformDurable extends TxnRequest<Reply> implements PreLoadContext
             shard = scope.homeKey().equals(progressKey) ? Home : Local;
         }
 
-        // TODO (soon): do not load from disk to perform this update
+        // TODO (expected, efficiency): do not load from disk to perform this update
         node.mapReduceConsumeLocal(contextFor(txnId), progressKey, at.epoch, this);
     }
 
diff --git a/accord-core/src/main/java/accord/messages/InformHomeDurable.java b/accord-core/src/main/java/accord/messages/InformHomeDurable.java
index cf7f611..a263cc7 100644
--- a/accord-core/src/main/java/accord/messages/InformHomeDurable.java
+++ b/accord-core/src/main/java/accord/messages/InformHomeDurable.java
@@ -31,7 +31,7 @@ public class InformHomeDurable implements Request
 
     public void process(Node node, Id replyToNode, ReplyContext replyContext)
     {
-        // TODO (soon): do not load txnId first
+        // TODO (expected, efficiency): do not load txnId first
         node.ifLocal(contextFor(txnId), homeKey, txnId.epoch, safeStore -> {
             Command command = safeStore.command(txnId);
             command.setDurability(safeStore, durability, homeKey, executeAt);
diff --git a/accord-core/src/main/java/accord/messages/InformOfTxnId.java b/accord-core/src/main/java/accord/messages/InformOfTxnId.java
index bfaea3b..a11b0db 100644
--- a/accord-core/src/main/java/accord/messages/InformOfTxnId.java
+++ b/accord-core/src/main/java/accord/messages/InformOfTxnId.java
@@ -24,7 +24,7 @@ public class InformOfTxnId extends AbstractEpochRequest<Reply> implements Reques
 
     public void process()
     {
-        // TODO (soon): do not first load txnId
+        // TODO (expected, efficiency): do not first load txnId
         node.mapReduceConsumeLocal(this, homeKey, txnId.epoch, this);
     }
 
@@ -70,8 +70,8 @@ public class InformOfTxnId extends AbstractEpochRequest<Reply> implements Reques
     @Override
     public Iterable<TxnId> txnIds()
     {
-        // TODO (soon): should be empty list, as can be written without existing state
-        //              (though perhaps might check existing in-memory state in case already present)
+        // TODO (expected, efficiency): should be empty list, as can be written without existing state
+        //                              (though perhaps might check existing in-memory state in case already present)
         return Collections.singleton(txnId);
     }
 }
diff --git a/accord-core/src/main/java/accord/messages/PreAccept.java b/accord-core/src/main/java/accord/messages/PreAccept.java
index 0a694b8..576797e 100644
--- a/accord-core/src/main/java/accord/messages/PreAccept.java
+++ b/accord-core/src/main/java/accord/messages/PreAccept.java
@@ -123,7 +123,7 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
     @Override
     public void accept(PreAcceptReply reply, Throwable failure)
     {
-        // TODO: communicate back the failure
+        // TODO (required, error handling): communicate back the failure
         node.reply(replyTo, replyContext, reply);
     }
 
diff --git a/accord-core/src/main/java/accord/messages/ReadData.java b/accord-core/src/main/java/accord/messages/ReadData.java
index e003bca..9471c8d 100644
--- a/accord-core/src/main/java/accord/messages/ReadData.java
+++ b/accord-core/src/main/java/accord/messages/ReadData.java
@@ -29,7 +29,6 @@ import accord.local.*;
 import accord.api.Data;
 import accord.topology.Topologies;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.Iterables;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,7 +41,7 @@ import static accord.messages.ReadData.ReadNack.Redundant;
 import static accord.messages.TxnRequest.*;
 import static accord.utils.MapReduceConsume.forEach;
 
-// TODO (soon): dedup - can currently have infinite pending reads that will be executed independently
+// TODO (required, efficiency): dedup - can currently have infinite pending reads that will be executed independently
 public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements CommandListener
 {
     private static final Logger logger = LoggerFactory.getLogger(ReadData.class);
@@ -56,10 +55,10 @@ public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements
     }
 
     public final long executeAtEpoch;
-    public final Seekables<?, ?> readScope; // TODO: this should be RoutingKeys as we have the Keys locally - but for simplicity for now we use Keys to implement keys()
+    public final Seekables<?, ?> readScope; // TODO (low priority, efficiency): this should be RoutingKeys, as we have the Keys locally, but for simplicity we use this to implement keys()
     private final long waitForEpoch;
     private Data data;
-    private transient boolean isObsolete; // TODO: respond with the Executed result we have stored?
+    private transient boolean isObsolete; // TODO (low priority, semantics): respond with the Executed result we have stored?
     private transient BitSet waitingOn;
     private transient int waitingOnCount;
 
@@ -193,10 +192,11 @@ public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements
         }
         else if (failure != null)
         {
-            // TODO (soon): test
+            // TODO (expected, testing): test
             node.reply(replyTo, replyContext, ReadNack.Error);
             data = null;
-            node.agent().onUncaughtException(failure); // TODO: probably a better way to handle this, as might not be uncaught
+            // TODO (expected, exceptions): probably a better way to handle this, as might not be uncaught
+            node.agent().onUncaughtException(failure);
             node.commandStores().mapReduceConsume(this, waitingOn.stream(), forEach(in -> in.command(txnId).removeListener(this), node.agent()));
         }
         else
@@ -229,7 +229,7 @@ public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements
         command.read(safeStore).addCallback((next, throwable) -> {
             if (throwable != null)
             {
-                // TODO (now): exception integration (non-trivial, as might be handled)
+                // TODO (expected, exceptions): should send exception to client, and consistency handle/propagate locally
                 logger.trace("{}: read failed for {}: {}", txnId, unsafeStore, throwable);
                 node.reply(replyTo, replyContext, ReadNack.Error);
             }
diff --git a/accord-core/src/main/java/accord/messages/TxnRequest.java b/accord-core/src/main/java/accord/messages/TxnRequest.java
index c34fa05..b80968d 100644
--- a/accord-core/src/main/java/accord/messages/TxnRequest.java
+++ b/accord-core/src/main/java/accord/messages/TxnRequest.java
@@ -42,7 +42,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
 {
     public static abstract class WithUnsynced<R> extends TxnRequest<R>
     {
-        public final long minEpoch; // TODO: can this just always be TxnId.epoch?
+        public final long minEpoch; // TODO (low priority, clarity): can this just always be TxnId.epoch?
         public final boolean doNotComputeProgressKey;
 
         public WithUnsynced(Id to, Topologies topologies, TxnId txnId, FullRoute<?> route)
@@ -56,20 +56,20 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
             this.minEpoch = topologies.oldestEpoch();
             this.doNotComputeProgressKey = doNotComputeProgressKey(topologies, startIndex, txnId, waitForEpoch());
 
-            // TODO (now): alongside Invariants class, introduce PARANOID mode for checking extra invariants
             Ranges ranges = topologies.forEpoch(txnId.epoch).rangesForNode(to);
             if (doNotComputeProgressKey)
             {
                 Invariants.checkState(!route.intersects(ranges)); // confirm dest is not a replica on txnId.epoch
             }
-            else
+            else if (Invariants.isParanoid())
             {
-                boolean intersects = route.intersects(ranges);
+                // check that the destination's newer topology does not yield different ranges
                 long progressEpoch = Math.min(waitForEpoch(), txnId.epoch);
                 Ranges computesRangesOn = topologies.forEpoch(progressEpoch).rangesForNode(to);
-                boolean check = computesRangesOn != null && route.intersects(computesRangesOn);
-                if (check != intersects)
-                    throw new IllegalStateException();
+                if (computesRangesOn == null)
+                    Invariants.checkState(!route.intersects(ranges));
+                else
+                    Invariants.checkState(route.slice(computesRangesOn).equals(route.slice(ranges)));
             }
         }
 
@@ -140,7 +140,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
         this.node = on;
         this.replyTo = replyTo;
         this.replyContext = replyContext;
-        this.progressKey = progressKey(node); // TODO: not every class that extends TxnRequest needs this set
+        this.progressKey = progressKey(node); // TODO (low priority, clarity): not every class that extends TxnRequest needs this set
         process();
     }
 
@@ -231,7 +231,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
         return computeScope(node, topologies, route, startIndex, Route::slice, PartialRoute::union);
     }
 
-    // TODO: move to Topologies
+    // TODO (low priority, clarity): move to Topologies
     public static <I, O> O computeScope(Node.Id node, Topologies topologies, I keys, int startIndex, BiFunction<I, Ranges, O> slice, BiFunction<O, O, O> merge)
     {
         O scope = computeScopeInternal(node, topologies, keys, startIndex, slice, merge);
@@ -275,7 +275,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
         // So in these cases we send a special flag indicating that the progress key should not be computed
         // (as it might be done so with stale ring information)
 
-        // TODO (soon): this would be better defined as "hasProgressKey"
+        // TODO (low priority, clarity): this would be better defined as "hasProgressKey"
         return waitForEpoch < txnId.epoch && startIndex > 0
                 && topologies.get(startIndex).epoch() < txnId.epoch;
     }
diff --git a/accord-core/src/main/java/accord/primitives/AbstractKeys.java b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
index 6886848..b777696 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractKeys.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
@@ -16,7 +16,7 @@ import net.nicoulaj.compilecommand.annotations.Inline;
 import static accord.primitives.Routable.Kind.Key;
 
 @SuppressWarnings("rawtypes")
-// TODO: check that foldl call-sites are inlined and optimised by HotSpot
+// TODO (desired, efficiency): check that foldl call-sites are inlined and optimised by HotSpot
 public abstract class AbstractKeys<K extends RoutableKey, KS extends Routables<K, ?>> implements Iterable<K>, Routables<K, KS>
 {
     final K[] keys;
@@ -72,9 +72,8 @@ public abstract class AbstractKeys<K extends RoutableKey, KS extends Routables<K
     }
 
     @Override
-    public boolean intersects(AbstractRanges<?> ranges)
+    public final boolean intersects(AbstractRanges<?> ranges)
     {
-        // TODO (now): make this final
         return ranges.intersects(this);
     }
 
@@ -144,7 +143,7 @@ public abstract class AbstractKeys<K extends RoutableKey, KS extends Routables<K
         return stream().map(Object::toString).collect(Collectors.joining(",", "[", "]"));
     }
 
-    // TODO (now): accept cached buffers
+    // TODO (expected, efficiency): accept cached buffers
     protected K[] slice(Ranges ranges, IntFunction<K[]> factory)
     {
         return SortedArrays.sliceWithMultipleMatches(keys, ranges.ranges, factory, (k, r) -> -r.compareTo(k), Range::compareTo);
diff --git a/accord-core/src/main/java/accord/primitives/AbstractRanges.java b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
index 44e1220..a87adce 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractRanges.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
@@ -230,7 +230,7 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
      * terminating at the first indexes where this ceases to be true
      * @return index of {@code as} in upper 32bits, {@code bs} in lower 32bits
      *
-     * TODO: better support for merging runs of overlapping or adjacent ranges
+     * TODO (low priority, efficiency): better support for merging runs of overlapping or adjacent ranges
      */
     static long supersetLinearMerge(Range[] as, Range[] bs)
     {
@@ -262,7 +262,7 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
             {
                 // use a temporary counter, so that if we don't find a run of ranges that enforce the superset
                 // condition we exit at the start of the mismatch run (and permit it to be merged)
-                // TODO: use exponentialSearch
+                // TODO (easy, efficiency): use exponentialSearch
                 int tmpai = ai;
                 do
                 {
@@ -313,7 +313,7 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
         if (bi == bs.length)
             return constructor.construct(param1, param2, (as == left.ranges ? left : right).ranges);
 
-        // TODO (now): caching
+        // TODO (expected, efficiency): ArrayBuffers caching
         Range[] result = new Range[as.length + (bs.length - bi)];
         int resultCount;
         switch (mode)
@@ -345,7 +345,7 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
             }
             else
             {
-                // TODO: we don't seem to currently merge adjacent (but non-overlapping)
+                // TODO (desired, efficiency/semantics): we don't seem to currently merge adjacent (but non-overlapping)
                 RoutingKey start = a.start().compareTo(b.start()) <= 0 ? a.start() : b.start();
                 RoutingKey end = a.end().compareTo(b.end()) >= 0 ? a.end() : b.end();
                 ai++;
@@ -411,7 +411,6 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
         if (ranges.length == 0)
             return input;
 
-        // TODO: use cache
         ObjectBuffers<Range> cachedKeyRanges = cachedRanges();
         Range[] buffer = cachedKeyRanges.get(ranges.length);
         try
diff --git a/accord-core/src/main/java/accord/primitives/Deps.java b/accord-core/src/main/java/accord/primitives/Deps.java
index ff0f7a0..f72b33b 100644
--- a/accord-core/src/main/java/accord/primitives/Deps.java
+++ b/accord-core/src/main/java/accord/primitives/Deps.java
@@ -40,7 +40,7 @@ import static accord.utils.SortedArrays.Search.FAST;
  * A collection of dependencies for a transaction, organised by the key the dependency is adopted via.
  * An inverse map from TxnId to Key may also be constructed and stored in this collection.
  */
-// TODO: switch to RoutingKey? Would mean adopting execution dependencies less precisely
+// TODO (desired, consider): switch to RoutingKey? Would mean adopting execution dependencies less precisely, but saving ser/deser of large keys
 public class Deps implements Iterable<Map.Entry<Key, TxnId>>
 {
     private static final boolean DEBUG_CHECKS = true;
@@ -84,7 +84,7 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
         return new OrderedBuilder(hasOrderedTxnId);
     }
 
-    // TODO: cache this object to reduce setup/teardown and allocation
+    // TODO (expected, efficiency): cache this object per thread
     public static abstract class AbstractOrderedBuilder<T extends Deps> implements AutoCloseable
     {
         final ObjectBuffers<TxnId> cachedTxnIds = cachedTxnIds();
@@ -152,7 +152,7 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
 
             if (totalCount != keyOffset && !hasOrderedTxnId)
             {
-                // TODO: this allocates a significant amount of memory: would be preferable to be able to sort using a pre-defined scratch buffer
+                // TODO (low priority, efficiency): this allocates a significant amount of memory: would be preferable to be able to sort using a pre-defined scratch buffer
                 Arrays.sort(keyToTxnId, keyOffset, totalCount);
                 for (int i = keyOffset + 1 ; i < totalCount ; ++i)
                 {
@@ -411,7 +411,7 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
     }
 
     final Keys keys; // unique Keys
-    final TxnId[] txnIds; // unique TxnId TODO: this should perhaps be a BTree?
+    final TxnId[] txnIds; // unique TxnId TODO (low priority, efficiency): this could be a BTree?
 
     /**
      * This represents a map of {@code Key -> [TxnId] } where each TxnId is actually a pointer into the txnIds array.
@@ -445,7 +445,6 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
             checkValid();
     }
 
-    // TODO: offer option of computing the maximal KeyRanges that covers the same set of keys as covered by the parameter
     public PartialDeps slice(Ranges ranges)
     {
         if (isEmpty())
@@ -594,9 +593,9 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
         return true;
     }
 
-    // TODO: this method supports merging keyToTxnId OR txnIdToKey; we can perhaps save time and effort when constructing
-    //       Deps on remote hosts by only producing txnIdToKey with OrderedCollector and serializing only this,
-    //       and merging on the recipient before inverting, so that we only have to invert the final assembled deps
+    // TODO (low priority, efficiency): this method supports merging keyToTxnId OR txnIdToKey; we can perhaps save time
+    //  and effort when constructing Deps on remote hosts by only producing txnIdToKey with OrderedCollector and serializing
+    //  only this, and merging on the recipient before inverting, so that we only have to invert the final assembled deps
     private static <K extends Comparable<? super K>, V extends Comparable<? super V>, T>
     T linearUnion(K[] leftKeys, int leftKeysLength, V[] leftValues, int leftValuesLength, int[] left, int leftLength,
                   K[] rightKeys, int rightKeysLength, V[] rightValues, int rightValuesLength, int[] right, int rightLength,
@@ -609,7 +608,6 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
 
         try
         {
-            // TODO: this is a little clunky for getting back the buffer and its length
             outKeys = SortedArrays.linearUnion(leftKeys, leftKeysLength, rightKeys, rightKeysLength, keyBuffers);
             outKeysLength = keyBuffers.lengthOfLast(outKeys);
             outValues = SortedArrays.linearUnion(leftValues, leftValuesLength, rightValues, rightValuesLength, valueBuffers);
@@ -878,7 +876,6 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
         return result;
     }
 
-    // TODO: optimise for case where none removed
     public Deps without(Predicate<TxnId> remove)
     {
         if (isEmpty())
@@ -1077,8 +1074,8 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
         // Find all keys within the ranges, but record existence within an int64 bitset.  Since the bitset is limited
         // to 64, this search must be called multiple times searching for different TxnIds in txnIds; this also has
         // the property that forEach is called in TxnId order.
-        //TODO Should TxnId order be part of the public docs or just a hidden implementation detail?  The only caller
-        // does not rely on this ordering.
+        //TODO (expected, efficiency): reconsider this, probably not worth trying to save allocations at cost of multiple loop
+        //                             use BitSet, or perhaps extend so we can have no nested allocations when few bits
         for (int offset = 0 ; offset < txnIds.length ; offset += 64)
         {
             long bitset = Routables.foldl(keys, ranges, (key, off, value, keyIndex) -> {
@@ -1086,7 +1083,7 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
                 int end = endOffset(keyIndex);
                 if (off > 0)
                 {
-                    // TODO: interpolation search probably great here
+                    // TODO (low priority, efficiency): interpolation search probably great here
                     index = Arrays.binarySearch(keyToTxnId, index, end, (int)off);
                     if (index < 0)
                         index = -1 - index;
diff --git a/accord-core/src/main/java/accord/primitives/FullKeyRoute.java b/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
index cd9f3a9..469ed59 100644
--- a/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
+++ b/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
@@ -30,18 +30,10 @@ public class FullKeyRoute extends KeyRoute implements FullRoute<RoutingKey>
         return true;
     }
 
-    @Override
-    public boolean intersects(AbstractRanges<?> ranges)
-    {
-        // TODO (now): remove this in favour of parent implementation - ambiguous at present
-        return true;
-    }
-
     @Override
     public FullKeyRoute with(RoutingKey withKey)
     {
         Invariants.checkArgument(contains(withKey));
-        // TODO (now): remove this in favour of parent implementation - ambiguous at present
         return this;
     }
 
diff --git a/accord-core/src/main/java/accord/primitives/Keys.java b/accord-core/src/main/java/accord/primitives/Keys.java
index 2d9c7b8..a4f9cee 100644
--- a/accord-core/src/main/java/accord/primitives/Keys.java
+++ b/accord-core/src/main/java/accord/primitives/Keys.java
@@ -27,8 +27,7 @@ import accord.utils.ArrayBuffers.ObjectBuffers;
 
 import static accord.utils.ArrayBuffers.cachedKeys;
 
-// TODO: this should probably be a BTree
-// TODO: check that foldl call-sites are inlined and optimised by HotSpot
+// TODO (low priority, efficiency): this should probably be a BTree
 public class Keys extends AbstractKeys<Key, Keys> implements Seekables<Key, Keys>
 {
     public static class SerializationSupport
diff --git a/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java b/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
index c964aef..da9143a 100644
--- a/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
+++ b/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
@@ -54,13 +54,6 @@ public class PartialKeyRoute extends KeyRoute implements PartialRoute<RoutingKey
         return covering.containsAll(ranges);
     }
 
-    @Override
-    public boolean intersects(AbstractRanges<?> ranges)
-    {
-        // TODO (now): remove this in favour of parent implementation - ambiguous at present
-        return ranges.intersects(covering);
-    }
-
     @Override
     public AbstractRoutableKeys<?> with(RoutingKey withKey)
     {
diff --git a/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java b/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
index 1cad856..42a9aac 100644
--- a/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
+++ b/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
@@ -61,7 +61,6 @@ public class PartialRangeRoute extends RangeRoute implements PartialRoute<Range>
     @Override
     public Unseekables<Range, ?> toMaximalUnseekables()
     {
-        // TODO (now)
         throw new UnsupportedOperationException();
     }
 
@@ -75,7 +74,6 @@ public class PartialRangeRoute extends RangeRoute implements PartialRoute<Range>
 
     public Unseekables<Range, ?> with(RoutingKey withKey)
     {
-        // TODO: this is left unimplemented until we actually have a range transaction to decide how best to address it
         throw new UnsupportedOperationException();
     }
 
diff --git a/accord-core/src/main/java/accord/primitives/PartialTxn.java b/accord-core/src/main/java/accord/primitives/PartialTxn.java
index 3da4715..f9588b1 100644
--- a/accord-core/src/main/java/accord/primitives/PartialTxn.java
+++ b/accord-core/src/main/java/accord/primitives/PartialTxn.java
@@ -9,7 +9,7 @@ import accord.api.Update;
 public interface PartialTxn extends Txn
 {
     Ranges covering();
-    // TODO: merge efficient merge when more than one input
+    // TODO (low priority, efficiency): efficient merge when more than one input
     PartialTxn with(PartialTxn add);
     Txn reconstitute(FullRoute<?> route);
     PartialTxn reconstitutePartial(PartialRoute<?> route);
@@ -28,12 +28,12 @@ public interface PartialTxn extends Txn
         return covering().containsAll(unseekables);
     }
 
-    // TODO: override toString
     static PartialTxn merge(@Nullable PartialTxn a, @Nullable PartialTxn b)
     {
         return a == null ? b : b == null ? a : a.with(b);
     }
 
+    // TODO (low priority, clarity): override toString
     class InMemory extends Txn.InMemory implements PartialTxn
     {
         public final Ranges covering;
@@ -50,7 +50,6 @@ public interface PartialTxn extends Txn
             return covering;
         }
 
-        // TODO: merge efficient merge when more than one input
         public PartialTxn with(PartialTxn add)
         {
             if (!add.kind().equals(kind()))
@@ -74,8 +73,6 @@ public interface PartialTxn extends Txn
             return new PartialTxn.InMemory(covering, kind(), keys, read, query, update);
         }
 
-        // TODO: override toString
-
         public boolean covers(Ranges ranges)
         {
             return covering.containsAll(ranges);
diff --git a/accord-core/src/main/java/accord/primitives/ProgressToken.java b/accord-core/src/main/java/accord/primitives/ProgressToken.java
index 02a4646..3f3b05a 100644
--- a/accord-core/src/main/java/accord/primitives/ProgressToken.java
+++ b/accord-core/src/main/java/accord/primitives/ProgressToken.java
@@ -50,7 +50,6 @@ public class ProgressToken implements Comparable<ProgressToken>, Outcome
     {
         Durability durability = this.durability.compareTo(that.durability) >= 0 ? this.durability : that.durability;
         Status status = this.status.compareTo(that.status) >= 0 ? this.status : that.status;
-        // TODO: slightly inefficient
         Ballot promised = this.promised.compareTo(that.promised) >= 0 ? this.promised : that.promised;
         boolean isAccepted = (this.isAccepted && this.promised.equals(promised)) || (that.isAccepted && that.promised.equals(promised));
         if (isSame(durability, status, promised, isAccepted))
diff --git a/accord-core/src/main/java/accord/primitives/RoutableKey.java b/accord-core/src/main/java/accord/primitives/RoutableKey.java
index b6ba11b..4e90fe0 100644
--- a/accord-core/src/main/java/accord/primitives/RoutableKey.java
+++ b/accord-core/src/main/java/accord/primitives/RoutableKey.java
@@ -12,7 +12,7 @@ public interface RoutableKey extends Routable, Comparable<RoutableKey>
      *
      * All RoutingKey implementations must sort correctly with this type.
      *
-     * TODO: need to partition range from/to -/+ infinity as otherwise we exclude at least one key
+     * TODO (expected, testing): need to partition range from/to -/+ infinity as otherwise we exclude at least one key
      */
     class InfiniteRoutableKey implements RoutableKey
     {
diff --git a/accord-core/src/main/java/accord/topology/Shard.java b/accord-core/src/main/java/accord/topology/Shard.java
index c749d40..bb22017 100644
--- a/accord-core/src/main/java/accord/topology/Shard.java
+++ b/accord-core/src/main/java/accord/topology/Shard.java
@@ -33,11 +33,11 @@ import com.google.common.collect.Iterables;
 
 import static accord.utils.Invariants.checkArgument;
 
-// TODO: concept of region/locality
+// TODO (expected, efficiency): concept of region/locality
 public class Shard
 {
     public final Range range;
-    // TODO: use BTreeSet to combine these two (or introduce version that operates over long values)
+    // TODO (desired, clarity): use BTreeSet to combine these two (or introduce version that operates over long values)
     public final List<Id> nodes;
     public final Set<Id> nodeSet;
     public final Set<Id> fastPathElectorate;
diff --git a/accord-core/src/main/java/accord/topology/Topologies.java b/accord-core/src/main/java/accord/topology/Topologies.java
index f799193..13a00ad 100644
--- a/accord-core/src/main/java/accord/topology/Topologies.java
+++ b/accord-core/src/main/java/accord/topology/Topologies.java
@@ -28,8 +28,8 @@ import accord.utils.Invariants;
 
 import java.util.*;
 
-// TODO: we can probably most efficiently create a new synthetic Topology that applies for a range of epochs
-//       and permit Topology to implement it, so that
+// TODO (desired, efficiency/clarity): since Topologies are rarely needed, should optimise API for single topology case
+//  (e.g. at least implementing Topologies by Topology)
 public interface Topologies extends TopologySorter
 {
     Topology current();
diff --git a/accord-core/src/main/java/accord/topology/Topology.java b/accord-core/src/main/java/accord/topology/Topology.java
index 277446f..a502bd4 100644
--- a/accord-core/src/main/java/accord/topology/Topology.java
+++ b/accord-core/src/main/java/accord/topology/Topology.java
@@ -38,12 +38,14 @@ public class Topology
     final long epoch;
     final Shard[] shards;
     final Ranges ranges;
+    /**
+     * TODO (desired, efficiency): do not recompute nodeLookup for sub-topologies
+     */
     final Map<Id, NodeInfo> nodeLookup;
     /**
      * This array is used to permit cheaper sharing of Topology objects between requests, as we must only specify
      * the indexes within the parent Topology that we contain. This also permits us to perform efficient merges with
      * {@code NodeInfo.supersetIndexes} to find the shards that intersect a given node without recomputing the NodeInfo.
-     * TODO: do not recompute nodeLookup
      */
     final Ranges subsetOfRanges;
     final int[] supersetIndexes;
@@ -215,7 +217,6 @@ public class Topology
     {
         Ranges rangeSubset = ranges.select(newSubset);
 
-        // TODO: more efficient sharing of nodeLookup state
         Map<Id, NodeInfo> nodeLookup = new HashMap<>();
         for (int shardIndex : newSubset)
         {
@@ -229,8 +230,6 @@ public class Topology
     private Topology forSubset(int[] newSubset, Collection<Id> nodes)
     {
         Ranges rangeSubset = ranges.select(newSubset);
-
-        // TODO: more efficient sharing of nodeLookup state
         Map<Id, NodeInfo> nodeLookup = new HashMap<>();
         for (Id id : nodes)
             nodeLookup.put(id, this.nodeLookup.get(id));
@@ -395,40 +394,9 @@ public class Topology
         return initialValue;
     }
 
-    public int matchesOn(Id on, IndexedPredicate<Shard> consumer)
-    {
-        // TODO: this can be done by divide-and-conquer splitting of the lists and recursion, which should be more efficient
-        int count = 0;
-        NodeInfo info = nodeLookup.get(on);
-        if (info == null)
-            return 0;
-        int[] a = supersetIndexes, b = info.supersetIndexes;
-        int ai = 0, bi = 0;
-        while (ai < a.length && bi < b.length)
-        {
-            if (a[ai] == b[bi])
-            {
-                if (consumer.test(shards[a[ai]], ai))
-                    ++count;
-                ++ai; ++bi;
-            }
-            else if (a[ai] < b[bi])
-            {
-                ai = exponentialSearch(a, ai + 1, a.length, b[bi]);
-                if (ai < 0) ai = -1 -ai;
-            }
-            else
-            {
-                bi = exponentialSearch(b, bi + 1, b.length, a[ai]);
-                if (bi < 0) bi = -1 -bi;
-            }
-        }
-        return count;
-    }
-
     public <P> int foldlIntOn(Id on, IndexedIntFunction<P> consumer, P param, int offset, int initialValue, int terminalValue)
     {
-        // TODO: this can be done by divide-and-conquer splitting of the lists and recursion, which should be more efficient
+        // TODO (low priority, efficiency/clarity): use findNextIntersection?
         NodeInfo info = nodeLookup.get(on);
         if (info == null)
             return initialValue;
diff --git a/accord-core/src/main/java/accord/topology/TopologyManager.java b/accord-core/src/main/java/accord/topology/TopologyManager.java
index ec1061f..38d6589 100644
--- a/accord-core/src/main/java/accord/topology/TopologyManager.java
+++ b/accord-core/src/main/java/accord/topology/TopologyManager.java
@@ -48,8 +48,8 @@ import static accord.utils.Invariants.checkArgument;
  *
  * Assumes a topology service that won't report epoch n without having n-1 etc also available
  *
- * TODO: make TopologyManager a Topologies and copy-on-write update to it, so we can always just take a reference for
- *       transactions instead of copying every time (and index into it by the txnId.epoch)
+ * TODO (desired, efficiency/clarity): make TopologyManager a Topologies and copy-on-write update to it,
+ *  so we can always just take a reference for transactions instead of copying every time (and index into it by the txnId.epoch)
  */
 public class TopologyManager implements ConfigurationService.Listener
 {
diff --git a/accord-core/src/main/java/accord/utils/ArrayBuffers.java b/accord-core/src/main/java/accord/utils/ArrayBuffers.java
index 2dce966..d1ad72a 100644
--- a/accord-core/src/main/java/accord/utils/ArrayBuffers.java
+++ b/accord-core/src/main/java/accord/utils/ArrayBuffers.java
@@ -51,7 +51,7 @@ public class ArrayBuffers
 {
     private static final boolean FULLY_UNCACHED = true;
 
-    // TODO: we should periodically clear the thread locals to ensure we aren't slowly accumulating unnecessarily large objects on every thread
+    // TODO (low priority, efficiency): we should periodically clear the thread locals to ensure we aren't slowly accumulating unnecessarily large objects on every thread
     private static final ThreadLocal<IntBufferCache> INTS = ThreadLocal.withInitial(() -> new IntBufferCache(4, 1 << 14));
     private static final ThreadLocal<ObjectBufferCache<Key>> KEYS = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 9, Key[]::new));
     private static final ThreadLocal<ObjectBufferCache<RoutingKey>> ROUTINGKEYS = ThreadLocal.withInitial(() -> new ObjectBufferCache<>(3, 1 << 9, RoutingKey[]::new));
diff --git a/accord-core/src/main/java/accord/utils/DeterministicIdentitySet.java b/accord-core/src/main/java/accord/utils/DeterministicIdentitySet.java
index a8208a1..e380e39 100644
--- a/accord-core/src/main/java/accord/utils/DeterministicIdentitySet.java
+++ b/accord-core/src/main/java/accord/utils/DeterministicIdentitySet.java
@@ -38,7 +38,7 @@ public class DeterministicIdentitySet<T> extends AbstractSet<T>
         }
     }
 
-    // TODO: an identity hash map that doesn't mind concurrent modification / iteration
+    // TODO (low priority): an identity hash map that doesn't mind concurrent modification / iteration
     final IdentityHashMap<T, Entry<T>> lookup;
     final Entry<T> head = new Entry<T>(null);
 
diff --git a/accord-core/src/main/java/accord/utils/IndexedTriConsumer.java b/accord-core/src/main/java/accord/utils/IndexedTriConsumer.java
index 418c69c..4400ff8 100644
--- a/accord-core/src/main/java/accord/utils/IndexedTriConsumer.java
+++ b/accord-core/src/main/java/accord/utils/IndexedTriConsumer.java
@@ -1,6 +1,5 @@
 package accord.utils;
 
-// TODO (now): migrate to utils, but must standardise on parameter order with index last
 public interface IndexedTriConsumer<P1, P2, P3>
 {
     void accept(P1 p1, P2 p2, P3 p3, int index);
diff --git a/accord-core/src/main/java/accord/utils/IntrusiveLinkedList.java b/accord-core/src/main/java/accord/utils/IntrusiveLinkedList.java
index eadcb92..8b822e9 100644
--- a/accord-core/src/main/java/accord/utils/IntrusiveLinkedList.java
+++ b/accord-core/src/main/java/accord/utils/IntrusiveLinkedList.java
@@ -29,7 +29,7 @@ import static java.util.Spliterators.spliteratorUnknownSize;
  * A simple intrusive double-linked list for maintaining a list of tasks,
  * useful for invalidating queued ordered tasks
  *
- * TODO COPIED FROM CASSANDRA
+ * TODO (low priority): COPIED FROM CASSANDRA
  */
 
 @SuppressWarnings("unchecked")
diff --git a/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java b/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
index 767d262..c180cf1 100644
--- a/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
+++ b/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
@@ -19,7 +19,7 @@
 package accord.utils;
 
 /**
- * TODO COPIED FROM CASSANDRA
+ * TODO (low priority): COPIED FROM CASSANDRA
  */
 public abstract class IntrusiveLinkedListNode
 {
diff --git a/accord-core/src/main/java/accord/utils/Invariants.java b/accord-core/src/main/java/accord/utils/Invariants.java
index 4242c5b..de7f2b0 100644
--- a/accord-core/src/main/java/accord/utils/Invariants.java
+++ b/accord-core/src/main/java/accord/utils/Invariants.java
@@ -8,6 +8,11 @@ public class Invariants
 {
     private static final boolean PARANOID = true;
 
+    public static boolean isParanoid()
+    {
+        return PARANOID;
+    }
+
     public static <T1, T2 extends T1> T2 checkType(T1 cast)
     {
         return (T2)cast;
diff --git a/accord-core/src/main/java/accord/utils/MapReduce.java b/accord-core/src/main/java/accord/utils/MapReduce.java
index 04b0f04..5cc4adc 100644
--- a/accord-core/src/main/java/accord/utils/MapReduce.java
+++ b/accord-core/src/main/java/accord/utils/MapReduce.java
@@ -4,7 +4,7 @@ import java.util.function.Function;
 
 public interface MapReduce<I, O> extends Function<I, O>
 {
-    // TODO (soon): ensure mutual exclusivity when calling each of these methods
+    // TODO (desired, safety): ensure mutual exclusivity when calling each of these methods
     O apply(I in);
     O reduce(O o1, O o2);
 }
diff --git a/accord-core/src/main/java/accord/utils/SortedArrays.java b/accord-core/src/main/java/accord/utils/SortedArrays.java
index a0aa473..542f8e2 100644
--- a/accord-core/src/main/java/accord/utils/SortedArrays.java
+++ b/accord-core/src/main/java/accord/utils/SortedArrays.java
@@ -56,9 +56,9 @@ public class SortedArrays
      *
      * Otherwise, depending on {@code buffers}, a result buffer may itself be returned or a new array.
      *
-     * TODO: introduce exponential search optimised version
-     * TODO: also compare with Hwang and Lin algorithm
-     * TODO: could also compare with a recursive partitioning scheme like quicksort
+     * TODO (low priority, efficiency): introduce exponential search optimised version
+     *                                  also compare with Hwang and Lin algorithm
+     *                                  could also compare with a recursive partitioning scheme like quicksort
      * (note that dual exponential search is also an optimal algorithm, just seemingly ignored by the literature,
      * and may be in practice faster for lists that are more often similar in size, and only occasionally very different.
      * Without performing extensive analysis, exponential search likely has higher constant factors in terms of the
@@ -218,7 +218,7 @@ public class SortedArrays
      *
      * Otherwise, depending on {@code buffers}, a result buffer may itself be returned or a new array.
      *
-     * TODO: introduce exponential search optimised version
+     * TODO (low priority, efficiency): introduce exponential search optimised version
      */
     public static <T> T[] linearIntersection(T[] left, int leftLength, T[] right, int rightLength, Comparator<T> comparator, ObjectBuffers<T> buffers)
     {
@@ -321,7 +321,7 @@ public class SortedArrays
      *
      * Otherwise, depending on {@code buffers}, a result buffer may itself be returned or a new array.
      *
-     * TODO: introduce exponential search optimised version
+     * TODO (low priority, efficiency): introduce exponential search optimised version
      */
     public static <T2, T1 extends Comparable<? super T2>> T1[] linearIntersection(T1[] left, int leftLength, T2[] right, int rightLength, ObjectBuffers<T1> buffers)
     {
diff --git a/accord-core/src/main/java/accord/utils/Utils.java b/accord-core/src/main/java/accord/utils/Utils.java
index 6995c7d..0c5ff5a 100644
--- a/accord-core/src/main/java/accord/utils/Utils.java
+++ b/accord-core/src/main/java/accord/utils/Utils.java
@@ -21,7 +21,7 @@ package accord.utils;
 import java.util.*;
 import java.util.function.IntFunction;
 
-// TODO: remove when jdk8 support is dropped
+// TODO (low priority): remove when jdk8 support is dropped
 public class Utils
 {
     // reimplements Collection#toArray
diff --git a/accord-core/src/test/java/accord/KeysTest.java b/accord-core/src/test/java/accord/KeysTest.java
index d7867f2..c8f7650 100644
--- a/accord-core/src/test/java/accord/KeysTest.java
+++ b/accord-core/src/test/java/accord/KeysTest.java
@@ -250,8 +250,6 @@ public class KeysTest
         });
     }
 
-    //TODO test foldlIntersect
-
     private static Gen<List<Raw>> keysGen() {
         return Gens.lists(Gens.ints().between(-1000, 1000).map(IntKey::key))
                 .unique()
diff --git a/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java b/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
index 6cbb4d1..c50e4dd 100644
--- a/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
+++ b/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
@@ -65,7 +65,7 @@ public class BurnTestConfigurationService implements TestableConfigurationServic
 
     private static class EpochHistory
     {
-        // TODO: move pendingEpochs / FetchTopology into here?
+        // TODO (low priority): move pendingEpochs / FetchTopology into here?
         private final List<EpochState> epochs = new ArrayList<>();
 
         private long lastReceived = 0;
diff --git a/accord-core/src/test/java/accord/burn/TopologyUpdates.java b/accord-core/src/test/java/accord/burn/TopologyUpdates.java
index d3136bd..61aa6b9 100644
--- a/accord-core/src/test/java/accord/burn/TopologyUpdates.java
+++ b/accord-core/src/test/java/accord/burn/TopologyUpdates.java
@@ -89,7 +89,7 @@ public class TopologyUpdates
 
             if (minStatus == null || minStatus.phase.compareTo(status.phase) >= 0)
             {
-                // TODO: minStatus == null means we're sending redundant messages
+                // TODO (low priority): minStatus == null means we're sending redundant messages
                 onDone.accept(true);
                 return;
             }
diff --git a/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java b/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
index e4bcd94..51cf8cb 100644
--- a/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
+++ b/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
@@ -8,7 +8,6 @@ import org.junit.jupiter.api.Assertions;
 import java.util.ArrayList;
 import java.util.Random;
 
-// TODO: check fast path accounting
 public class InvalidationTrackerReconciler extends TrackerReconciler<InvalidationShardTracker, InvalidationTracker, InvalidationTrackerReconciler.Rsp>
 {
     enum Rsp { PROMISED_FAST, NOT_PROMISED_FAST, PROMISED_SLOW, NOT_PROMISED_SLOW, FAIL }
@@ -59,7 +58,7 @@ public class InvalidationTrackerReconciler extends TrackerReconciler<Invalidatio
             case NoChange:
                 Assertions.assertFalse(tracker.any(InvalidationShardTracker::isPromised)
                         && tracker.any(InvalidationShardTracker::isFastPathRejected));
-                // TODO: it would be nice for InvalidationShardTracker to respond as soon as no shards are able to promise, but would require significant refactoring
+                // TODO (low priority): it would be nice for InvalidationShardTracker to respond as soon as no shards are able to promise, but would require significant refactoring
 //                Assertions.assertTrue(tracker.any(InvalidationShardTracker::canPromise));
                 Assertions.assertFalse(tracker.all(InvalidationShardTracker::isDecided));
         }
diff --git a/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java b/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
index 5cb0fab..30bcdfc 100644
--- a/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
+++ b/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.Assertions;
 import java.util.ArrayList;
 import java.util.Random;
 
-// TODO: check fast path accounting
+// TODO (required, testing): check fast path accounting
 public class RecoveryTrackerReconciler extends TrackerReconciler<RecoveryShardTracker, RecoveryTracker, RecoveryTrackerReconciler.Rsp>
 {
     enum Rsp { FAST, SLOW, FAIL }
diff --git a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
index fc569cf..015d798 100644
--- a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
+++ b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
@@ -76,8 +76,8 @@ public abstract class TrackerReconciler<ST extends ShardTracker, T extends Abstr
                 .collect(Collectors.toList());
     }
 
-    // TODO: generalise and parameterise topology generation a bit more
-    // TODO: select a subset of the generated topologies to correctly simulate topology consumption logic
+    // TODO (required, testing): generalise and parameterise topology generation a bit more
+    //                           also, select a subset of the generated topologies to correctly simulate topology consumption logic
     private static Stream<Topologies> topologies(Random random)
     {
         TopologyFactory factory = new TopologyFactory(2 + random.nextInt(3), IntHashKey.ranges(4 + random.nextInt(12)));
diff --git a/accord-core/src/test/java/accord/impl/TopologyFactory.java b/accord-core/src/test/java/accord/impl/TopologyFactory.java
index baaef2f..10ac252 100644
--- a/accord-core/src/test/java/accord/impl/TopologyFactory.java
+++ b/accord-core/src/test/java/accord/impl/TopologyFactory.java
@@ -31,7 +31,6 @@ import static accord.utils.Utils.toArray;
 public class TopologyFactory
 {
     public final int rf;
-    // TODO: convert to KeyRanges
     public final Range[] shardRanges;
 
     public TopologyFactory(int rf, Range... shardRanges)
diff --git a/accord-core/src/test/java/accord/impl/basic/Cluster.java b/accord-core/src/test/java/accord/impl/basic/Cluster.java
index f6afbdf..6ec6bf1 100644
--- a/accord-core/src/test/java/accord/impl/basic/Cluster.java
+++ b/accord-core/src/test/java/accord/impl/basic/Cluster.java
@@ -132,7 +132,7 @@ public class Cluster implements Scheduler
             Packet deliver = (Packet) next;
             Node on = lookup.apply(deliver.dst);
 
-            // TODO (soon): random drop chance independent of partition; also port flaky connections etc. from simulator
+            // TODO (required, testing): random drop chance independent of partition; also port flaky connections etc. from simulator
             // Drop the message if it goes across the partition
             boolean drop = ((Packet) next).src.id >= 0 &&
                            !(partitionSet.contains(deliver.src) && partitionSet.contains(deliver.dst)
@@ -206,9 +206,6 @@ public class Cluster implements Scheduler
         run.run();
     }
 
-    // TODO: there may remain some inconsistency of execution, at least causing different partitions if prior runs have happened;
-    //       unclear what source is, but less frequence now we split cluster partitioning recurring task to its own random
-    //       might be deterministic based on prior runs (some evidence of this), or non-deterministic
     public static void run(Id[] nodes, Supplier<PendingQueue> queueSupplier, Consumer<Packet> responseSink, Consumer<Throwable> onFailure, Supplier<Random> randomSupplier, Supplier<LongSupplier> nowSupplier, TopologyFactory topologyFactory, Supplier<Packet> in)
     {
         TopologyUpdates topologyUpdates = new TopologyUpdates();
@@ -249,7 +246,7 @@ public class Cluster implements Scheduler
             sinks.partitionSet = Collections.emptySet();
 
             // give progress log et al a chance to finish
-            // TODO: would be nice to make this more certain than an arbitrary number of additional rounds
+            // TODO (desired, testing): would be nice to make this more certain than an arbitrary number of additional rounds
             for (int i = 0 ; i < 10 ; ++i)
             {
                 sinks.processAll();
diff --git a/accord-core/src/test/java/accord/impl/list/ListAgent.java b/accord-core/src/test/java/accord/impl/list/ListAgent.java
index 16fcc2f..158c42b 100644
--- a/accord-core/src/test/java/accord/impl/list/ListAgent.java
+++ b/accord-core/src/test/java/accord/impl/list/ListAgent.java
@@ -58,7 +58,7 @@ public class ListAgent implements Agent
     @Override
     public void onUncaughtException(Throwable t)
     {
-        // TODO: ensure reported to runner
+        // TODO (required, testing): ensure reported to runner
         onFailure.accept(t);
     }
 
diff --git a/accord-core/src/test/java/accord/impl/list/ListRequest.java b/accord-core/src/test/java/accord/impl/list/ListRequest.java
index 05d9703..fa8a29e 100644
--- a/accord-core/src/test/java/accord/impl/list/ListRequest.java
+++ b/accord-core/src/test/java/accord/impl/list/ListRequest.java
@@ -102,7 +102,7 @@ public class ListRequest implements Request
         @Override
         public void accept(Result success, Throwable fail)
         {
-            // TODO: error handling
+            // TODO (desired, testing): error handling
             if (success != null)
             {
                 node.reply(client, replyContext, (ListResult) success);
diff --git a/accord-core/src/test/java/accord/impl/mock/MockCluster.java b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
index 9009898..7680042 100644
--- a/accord-core/src/test/java/accord/impl/mock/MockCluster.java
+++ b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
@@ -147,7 +147,7 @@ public class MockCluster implements Network, AutoCloseable, Iterable<Node>
 
         if (networkFilter.shouldDiscard(from, to, request))
         {
-            // TODO: more flexible timeouts
+            // TODO (desired, testing): more flexible timeouts
             if (callback != null)
                 callback.onFailure(to, new Timeout(null, null));
             logger.info("discarding filtered message from {} to {}: {}", from, to, request);
diff --git a/accord-core/src/test/java/accord/impl/mock/Network.java b/accord-core/src/test/java/accord/impl/mock/Network.java
index bed910c..4f64acc 100644
--- a/accord-core/src/test/java/accord/impl/mock/Network.java
+++ b/accord-core/src/test/java/accord/impl/mock/Network.java
@@ -54,13 +54,13 @@ public interface Network
         @Override
         public void send(Id from, Id to, Request request, Callback callback)
         {
-            // TODO: log
+            // TODO (easy, testing): log
         }
 
         @Override
         public void reply(Id from, Id replyingToNode, long replyingToMessage, Reply reply)
         {
-            // TODO: log
+            // TODO (easy, testing): log
         }
     };
 }
diff --git a/accord-core/src/test/java/accord/topology/TopologyRandomizer.java b/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
index 5d29f91..7942979 100644
--- a/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
+++ b/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
@@ -30,7 +30,7 @@ import org.slf4j.LoggerFactory;
 import java.util.*;
 import java.util.function.*;
 
-// TODO: add change replication factor
+// TODO (required, testing): add change replication factor
 public class TopologyRandomizer
 {
     private static final Logger logger = LoggerFactory.getLogger(TopologyRandomizer.class);
@@ -235,7 +235,7 @@ public class TopologyRandomizer
 
         Topology nextTopology = new Topology(current.epoch + 1, shards);
 
-        // FIXME: remove this (and the corresponding check in CommandStores) once lower bounds are implemented.
+        // TODO (expected, testing): remove this (and the corresponding check in CommandStores) once lower bounds are implemented.
         //  In the meantime, the logic needed to support acquiring ranges that we previously replicated is pretty
         //  convoluted without the ability to jettison epochs.
         if (reassignsRanges(current, shards, previouslyReplicated))
diff --git a/accord-core/src/test/java/accord/txn/DepsTest.java b/accord-core/src/test/java/accord/txn/DepsTest.java
index 00fbb64..9bd8d52 100644
--- a/accord-core/src/test/java/accord/txn/DepsTest.java
+++ b/accord-core/src/test/java/accord/txn/DepsTest.java
@@ -56,8 +56,8 @@ import static accord.utils.Gens.lists;
 import static accord.utils.Property.qt;
 import static accord.utils.Utils.toArray;
 
-// TODO: test Keys with no contents
-// TODO: test #without
+// TODO (expected, testing): test Keys with no contents, "without", "with" where TxnId and Keys are the same, but Key -> [TxnId] does not match;
+//  ensure high code coverage
 public class DepsTest
 {
     private static final Logger logger = LoggerFactory.getLogger(DepsTest.class);
@@ -270,13 +270,6 @@ public class DepsTest
         executor.shutdown();
     }
 
-    //TODO test "with" where TxnId and Keys are the same, but Key -> [TxnId] does not match
-
-    private static Keys keys(List<Deps> list)
-    {
-        return list.stream().map(d -> (Keys)d.test.keys()).reduce(Keys.EMPTY, Keys::union);
-    }
-
     private static void testMergedProperty(List<Deps> list)
     {
         Deps expected = Deps.merge(list);
diff --git a/accord-core/src/test/java/accord/verify/LinearizabilityVerifier.java b/accord-core/src/test/java/accord/verify/LinearizabilityVerifier.java
index 1c00ed7..125d416 100644
--- a/accord-core/src/test/java/accord/verify/LinearizabilityVerifier.java
+++ b/accord-core/src/test/java/accord/verify/LinearizabilityVerifier.java
@@ -34,7 +34,7 @@ import static accord.verify.LinearizabilityVerifier.Witness.Type.UPDATE_UNKNOWN;
  * We simply verify that there is no viewing of histories backwards or forwards in time (i.e. that the time periods each
  * unique list is witnessable for is disjoint) and that each list is a prefix of any lists witnessed later
  *
- * TODO: merge with SerializabilityVerifier.
+ * TODO (low priority): merge with SerializabilityVerifier.
  */
 public class LinearizabilityVerifier
 {
diff --git a/accord-core/src/test/java/accord/verify/SerializabilityVerifier.java b/accord-core/src/test/java/accord/verify/SerializabilityVerifier.java
index 00c7dc7..109a974 100644
--- a/accord-core/src/test/java/accord/verify/SerializabilityVerifier.java
+++ b/accord-core/src/test/java/accord/verify/SerializabilityVerifier.java
@@ -43,8 +43,7 @@ import java.util.stream.IntStream;
  * That is, we maintain links to the maximum predecessor step for each key, at each step for each key.
  * In combination with a linearizability verifier for each register/partition, we verify strict-serializability.
  *
- * TODO: session serializability, i.e. use each client's sequence of events as additional happens-before relations
- * TODO: find and report a path when we encounter a violation
+ * TODO (low priority): session serializability, i.e. use each client's sequence of events as additional happens-before relations
  */
 public class SerializabilityVerifier
 {
@@ -77,7 +76,6 @@ public class SerializabilityVerifier
         final int ofKey;
         final int ofStepIndex;
 
-        // TODO: we probably don't need this field, as it's implied by the node we point to, that we have when we enqueue refresh
         final int predecessorKey;
         int predecessorStepIndex;
 
@@ -187,8 +185,6 @@ public class SerializabilityVerifier
 
     /**
      *  The history of observations for a given key, or the set of nodes in the graph of observations for this key.
-     *
-     *  TODO: extend LinearizabilityVerifier
      */
     class Register
     {
@@ -409,7 +405,6 @@ public class SerializabilityVerifier
     final int keyCount;
     final Register[] registers;
 
-    // TODO: use another intrusive list or intrusive tree
     final TreeSet<MaxPredecessor> refresh = new TreeSet<>();
 
     // [key]->the sequence returned by any read performed in this transaction
diff --git a/accord-core/src/test/java/accord/verify/SerializabilityVerifierTest.java b/accord-core/src/test/java/accord/verify/SerializabilityVerifierTest.java
index f5310ea..1f85130 100644
--- a/accord-core/src/test/java/accord/verify/SerializabilityVerifierTest.java
+++ b/accord-core/src/test/java/accord/verify/SerializabilityVerifierTest.java
@@ -151,7 +151,7 @@ public class SerializabilityVerifierTest
         Observation[][] permuted = new Observation[permute.length][];
         for (int offset = 0 ; offset < permute.length ; ++offset)
         {
-            // TODO: more permutations
+            // TODO (low priority): more permutations
             for (int i = 0 ; i < permute.length ; ++i)
                 permuted[i] = permute[(offset + i) % permute.length];
 
diff --git a/accord-core/src/test/java/accord/verify/StrictSerializabilityVerifier.java b/accord-core/src/test/java/accord/verify/StrictSerializabilityVerifier.java
index 32d26a5..44374c1 100644
--- a/accord-core/src/test/java/accord/verify/StrictSerializabilityVerifier.java
+++ b/accord-core/src/test/java/accord/verify/StrictSerializabilityVerifier.java
@@ -48,7 +48,7 @@ import accord.utils.Invariants;
  * That is, we maintain links to the maximum predecessor step for each key, at each step for each key, and see if we can
  * find a path of predecessors that would witness us.
  *
- * TODO: find and report a path when we encounter a violation
+ * TODO (low priority): find and report a path when we encounter a violation
  */
 public class StrictSerializabilityVerifier
 {
@@ -83,7 +83,7 @@ public class StrictSerializabilityVerifier
         // the step index we are tracking predecessors for
         Step ofStep;
 
-        // TODO: we probably don't need this field, as it's implied by the node we point to, that we have when we enqueue refresh
+        // TODO (low priority): we probably don't need this field, as it's implied by the node we point to, that we have when we enqueue refresh
         // the key we are tracking the maximum predecessor for
         final int predecessorKey;
 
@@ -223,7 +223,7 @@ public class StrictSerializabilityVerifier
          */
         final MaxPredecessor[] maxPredecessors;
 
-        // TODO: cleanup
+        // TODO (low priority): cleanup
         List<UnknownStepHolder> unknownStepPeers;
         Map<Step, UnknownStepPredecessor> unknownStepPredecessors;
         Runnable onChange;
@@ -288,7 +288,7 @@ public class StrictSerializabilityVerifier
                 {
                     updated = true;
                     writtenAfter = start;
-                    // TODO: double check this will trigger an update of maxPredecessorX properties on each node with us as a maxPredecessor
+                    // TODO (low priority): double check this will trigger an update of maxPredecessorX properties on each node with us as a maxPredecessor
                 }
                 if (writtenAfter > writtenBefore)
                     throw new HistoryViolation(ofKey, "Write operation time conflicts with earlier read");
@@ -459,10 +459,7 @@ public class StrictSerializabilityVerifier
          * Any write value we don't know the step index for because we did not perform a coincident read;
          * we wait until we witness a read containing the
          *
-         * TODO: report a violation if we have witnessed a sequence missing any of these deferred operations
-         *       that started after they finished
-         *
-         * TODO: handle writes with unknown outcome
+         * TODO (desired, testing): handle writes with unknown outcome
          */
 
         final Map<Integer, UnknownStepHolder> byWriteValue = new HashMap<>();
@@ -530,7 +527,7 @@ public class StrictSerializabilityVerifier
                 throw new AssertionError();
             if (null != byTimestamp.putIfAbsent(end, unknownStep))
                 throw new AssertionError();
-            // TODO: verify this propagation by unit test
+            // TODO (desired, testing): verify this propagation by unit test
             unknownSteps[ofKey].step.receiveKnowledgePhasedPredecessors(this, StrictSerializabilityVerifier.this);
         }
 
@@ -701,7 +698,7 @@ public class StrictSerializabilityVerifier
     final int keyCount;
     final Register[] registers;
 
-    // TODO: use another intrusive list or intrusive tree
+    // TODO (low priority): use another intrusive list or intrusive tree
     final TreeSet<MaxPredecessor> refresh = new TreeSet<>();
 
     // [key]->the sequence returned by any read performed in this transaction
@@ -712,7 +709,7 @@ public class StrictSerializabilityVerifier
     final int[] bufNewPeerSteps;
     final UnknownStepHolder[] bufUnknownSteps;
 
-    // TODO (soon): verify operations with unknown outcomes are finalised by the first operation that starts after the coordinator abandons the txn
+    // TODO (desired, testing): verify operations with unknown outcomes are finalised by the first operation that starts after the coordinator abandons the txn
     public StrictSerializabilityVerifier(int keyCount)
     {
         this.keyCount = keyCount;
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
index bba292e..95130f5 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
@@ -47,7 +47,7 @@ import accord.messages.Request;
 import accord.api.Scheduler;
 import accord.topology.Topology;
 
-// TODO: merge with accord.impl.basic.Cluster
+// TODO (low priority, testing): merge with accord.impl.basic.Cluster
 public class Cluster implements Scheduler
 {
     public interface Queue<T>
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
index f311261..e374f01 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
@@ -489,7 +489,6 @@ public class Json
 
     static
     {
-        // TODO: Maelstrom hooks should be registered at run-time to permit separate tree
         GSON = new GsonBuilder().registerTypeAdapter(Packet.class, Packet.GSON_ADAPTER)
                                 .registerTypeAdapter(Id.class, ID_ADAPTER)
                                 .registerTypeAdapter(Txn.class, TXN_ADAPTER)
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromRequest.java b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromRequest.java
index b88891f..95c7709 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromRequest.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromRequest.java
@@ -47,7 +47,6 @@ public class MaelstromRequest extends Body implements Request
 
     public void process(Node node, Id client, ReplyContext replyContext)
     {
-        // TODO: error handling
         node.coordinate(txn).addCallback((success, fail) -> {
             if (success != null) node.reply(client, replyContext, new MaelstromReply(MaelstromReplyContext.messageIdFor(replyContext), (MaelstromResult) success));
 //            else node.reply(client, messageId, new Error(messageId, 13, fail.getMessage()));
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Value.java b/accord-maelstrom/src/main/java/accord/maelstrom/Value.java
index 55537b1..47e36a7 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Value.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Value.java
@@ -31,7 +31,6 @@ import com.google.gson.stream.JsonWriter;
 
 import static accord.utils.Utils.toArray;
 
-// TODO: abstract
 public class Value
 {
     public static final Value EMPTY = new Value();
diff --git a/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java b/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
index 9a4e926..f67b5bf 100644
--- a/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
+++ b/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
@@ -249,7 +249,7 @@ public class Runner
         Main.listen(factory, parseOutput(delay, output, Function.identity()), System.out, System.err);
     }
 
-    // TODO: we need to align response ids with the input; for now replies are broken
+    // TODO (low priority, maelstrom): we need to align response ids with the input; for now replies are broken
     static void replay(int nodeCount, TopologyFactory factory, boolean delay, Supplier<Packet> input) throws IOException
     {
         run(nodeCount, new QueueSupplier()


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org


[cassandra-accord] 01/03: Shard CommandStores by contiguous ranges (#20)

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

benedict pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git

commit 804a77d32c8ae45751a3a7f450b372560f08cacc
Author: Benedict Elliott Smith <be...@apache.org>
AuthorDate: Tue Jan 10 21:31:49 2023 +0000

    Shard CommandStores by contiguous ranges (#20)
---
 .../src/main/java/accord/coordinate/CheckOn.java   |   5 +-
 .../src/main/java/accord/coordinate/Execute.java   |   3 +
 .../java/accord/impl/InMemoryCommandStore.java     |  80 +++---
 .../java/accord/impl/InMemoryCommandStores.java    |  17 +-
 .../main/java/accord/impl/SimpleProgressLog.java   | 103 ++++----
 .../main/java/accord/local/AsyncCommandStores.java |   4 +-
 .../src/main/java/accord/local/Command.java        |  20 +-
 .../src/main/java/accord/local/CommandStore.java   |  51 +---
 .../src/main/java/accord/local/CommandStores.java  | 267 +++++++++------------
 accord-core/src/main/java/accord/local/Node.java   |   6 +-
 .../main/java/accord/local/SafeCommandStore.java   |   2 +-
 .../main/java/accord/local/ShardDistributor.java   |  95 ++++++++
 .../main/java/accord/local/SyncCommandStores.java  |   8 +-
 .../src/main/java/accord/messages/ReadData.java    |   2 -
 .../main/java/accord/primitives/AbstractKeys.java  |   8 -
 .../java/accord/primitives/AbstractRanges.java     |  29 +--
 .../src/main/java/accord/primitives/Deps.java      |   6 +-
 .../main/java/accord/primitives/FullKeyRoute.java  |   4 +-
 .../src/main/java/accord/primitives/Keys.java      |   2 +-
 .../java/accord/primitives/PartialKeyRoute.java    |  12 +-
 .../java/accord/primitives/PartialRangeRoute.java  |   6 +-
 .../src/main/java/accord/primitives/Range.java     |   8 +-
 .../main/java/accord/primitives/RoutableKey.java   |  10 -
 .../main/java/accord/primitives/RoutingKeys.java   |   2 +-
 .../src/main/java/accord/primitives/Txn.java       |   3 -
 .../src/main/java/accord/primitives/Writes.java    |   3 +-
 .../src/main/java/accord/topology/Shard.java       |   2 +-
 .../java/accord/utils/IntrusiveLinkedListNode.java |   1 +
 .../src/main/java/accord/utils/Invariants.java     |   8 +
 .../src/test/java/accord/impl/IntHashKey.java      |  91 +++++--
 accord-core/src/test/java/accord/impl/IntKey.java  |  69 +++++-
 .../src/test/java/accord/impl/basic/Cluster.java   |  11 +-
 .../test/java/accord/impl/mock/MockCluster.java    |   2 +
 .../src/test/java/accord/local/CommandTest.java    |   2 +-
 .../test/java/accord/messages/PreAcceptTest.java   |   1 +
 .../test/java/accord/topology/TopologyTest.java    |   6 +-
 accord-core/src/test/java/accord/txn/DepsTest.java |   8 +-
 .../src/test/java/accord/utils/RangeTest.java      |  38 +--
 .../src/main/java/accord/maelstrom/Cluster.java    |   7 +-
 .../src/main/java/accord/maelstrom/Json.java       |  10 +
 .../main/java/accord/maelstrom/MaelstromKey.java   |  85 ++++++-
 .../src/main/java/accord/maelstrom/Main.java       |   6 +-
 42 files changed, 645 insertions(+), 458 deletions(-)

diff --git a/accord-core/src/main/java/accord/coordinate/CheckOn.java b/accord-core/src/main/java/accord/coordinate/CheckOn.java
index 7819d9a..18ab6f9 100644
--- a/accord-core/src/main/java/accord/coordinate/CheckOn.java
+++ b/accord-core/src/main/java/accord/coordinate/CheckOn.java
@@ -216,10 +216,7 @@ public class CheckOn extends CheckShards
             if (!merged.durability.isDurable() || homeKey == null)
                 return null;
 
-            if (!safeStore.ranges().owns(txnId.epoch, homeKey))
-                return null;
-
-            if (!safeStore.commandStore().hashIntersects(homeKey))
+            if (!safeStore.ranges().at(txnId.epoch).contains(homeKey))
                 return null;
 
             Timestamp executeAt = merged.saveStatus.known.executeAt.isDecisionKnown() ? merged.executeAt : null;
diff --git a/accord-core/src/main/java/accord/coordinate/Execute.java b/accord-core/src/main/java/accord/coordinate/Execute.java
index 6195f73..03ee66c 100644
--- a/accord-core/src/main/java/accord/coordinate/Execute.java
+++ b/accord-core/src/main/java/accord/coordinate/Execute.java
@@ -104,6 +104,9 @@ class Execute extends ReadCoordinator<ReadReply>
         switch (nack)
         {
             default: throw new IllegalStateException();
+            case Error:
+                // TODO (expected): report content of error
+                return Action.Reject;
             case Redundant:
                 callback.accept(null, new Preempted(txnId, route.homeKey()));
                 return Action.Abort;
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
index ad08d14..f6b2d57 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
@@ -27,7 +27,6 @@ import accord.api.ProgressLog;
 import accord.impl.InMemoryCommandStore.SingleThread.AsyncState;
 import accord.impl.InMemoryCommandStore.Synchronized.SynchronizedState;
 import accord.local.Command;
-import accord.local.CommandStore.RangesForEpoch;
 import accord.local.CommandsForKey;
 import accord.local.CommandListener;
 import accord.local.Node;
@@ -35,9 +34,12 @@ import accord.local.NodeTimeService;
 import accord.local.PreLoadContext;
 import accord.local.SafeCommandStore;
 import accord.local.SyncCommandStores;
+import accord.local.CommandStores.RangesForEpochHolder;
+import accord.local.CommandStores.RangesForEpoch;
 import accord.primitives.Timestamp;
 import accord.primitives.TxnId;
 import accord.primitives.*;
+import accord.utils.Invariants;
 import org.apache.cassandra.utils.concurrent.AsyncPromise;
 import org.apache.cassandra.utils.concurrent.Future;
 
@@ -60,19 +62,20 @@ public class InMemoryCommandStore
         private final Agent agent;
         private final DataStore store;
         private final ProgressLog progressLog;
-        private final RangesForEpoch rangesForEpoch;
+        private final RangesForEpochHolder rangesForEpochHolder;
+        private RangesForEpoch rangesForEpoch;
 
         private final CommandStore commandStore;
         private final NavigableMap<TxnId, Command> commands = new TreeMap<>();
         private final NavigableMap<RoutableKey, InMemoryCommandsForKey> commandsForKey = new TreeMap<>();
 
-        public State(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpoch rangesForEpoch, CommandStore commandStore)
+        public State(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpochHolder rangesForEpoch, CommandStore commandStore)
         {
             this.time = time;
             this.agent = agent;
             this.store = store;
             this.progressLog = progressLog;
-            this.rangesForEpoch = rangesForEpoch;
+            this.rangesForEpochHolder = rangesForEpoch;
             this.commandStore = commandStore;
         }
 
@@ -144,6 +147,7 @@ public class InMemoryCommandStore
         @Override
         public RangesForEpoch ranges()
         {
+            Invariants.checkState(rangesForEpoch != null);
             return rangesForEpoch;
         }
 
@@ -164,6 +168,11 @@ public class InMemoryCommandStore
             return time.uniqueNow(max);
         }
 
+        void refreshRanges()
+        {
+            rangesForEpoch = rangesForEpochHolder.get();
+        }
+
         @Override
         public NodeTimeService time()
         {
@@ -185,9 +194,10 @@ public class InMemoryCommandStore
                         range.start(), range.startInclusive(),
                         range.end(), range.endInclusive()
                 ).values();
+
                 for (InMemoryCommandsForKey commands : rangeCommands)
                 {
-                    commands.forWitnessed(minTimestamp, maxTimestamp, cmd -> consumer.accept((Command) cmd));
+                    commands.forWitnessed(minTimestamp, maxTimestamp, consumer);
                 }
             }
         }
@@ -204,9 +214,9 @@ public class InMemoryCommandStore
                         range.endInclusive()).values();
                 for (InMemoryCommandsForKey commands : rangeCommands)
                 {
-                    Collection<Command> committed = commands.committedByExecuteAt()
-                            .between(minTimestamp, maxTimestamp).map(cmd -> (Command) cmd).collect(Collectors.toList());
-                    committed.forEach(consumer);
+                    commands.committedByExecuteAt()
+                            .between(minTimestamp, maxTimestamp)
+                            .forEach(consumer);
                 }
             }
         }
@@ -221,7 +231,6 @@ public class InMemoryCommandStore
                     AbstractKeys<Key, ?> keys = (AbstractKeys<Key, ?>) keysOrRanges;
                     return keys.stream()
                             .filter(slice::contains)
-                            .filter(commandStore::hashIntersects)
                             .map(this::commandsForKey)
                             .map(map)
                             .reduce(initialValue, reduce);
@@ -241,10 +250,7 @@ public class InMemoryCommandStore
                     throw new AssertionError();
                 case Key:
                     AbstractKeys<Key, ?> keys = (AbstractKeys<Key, ?>) keysOrRanges;
-                    keys.forEach(slice, key -> {
-                        if (commandStore.hashIntersects(key))
-                            forEach.accept(commandsForKey(key));
-                    });
+                    keys.forEach(slice, key -> forEach.accept(commandsForKey(key)));
                     break;
                 case Range:
                     Ranges ranges = (Ranges) keysOrRanges;
@@ -281,7 +287,7 @@ public class InMemoryCommandStore
     {
         public static class SynchronizedState extends State implements SyncCommandStores.SafeSyncCommandStore
         {
-            public SynchronizedState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpoch rangesForEpoch, CommandStore commandStore)
+            public SynchronizedState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpochHolder rangesForEpoch, CommandStore commandStore)
             {
                 super(time, agent, store, progressLog, rangesForEpoch, commandStore);
             }
@@ -315,9 +321,9 @@ public class InMemoryCommandStore
 
         final SynchronizedState state;
 
-        public Synchronized(int id, int generation, int shardIndex, int numShards, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpoch rangesForEpoch)
+        public Synchronized(int id, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpochHolder rangesForEpoch)
         {
-            super(id, generation, shardIndex, numShards);
+            super(id);
             this.state = new SynchronizedState(time, agent, store, progressLogFactory.create(this), rangesForEpoch, this);
         }
 
@@ -327,22 +333,28 @@ public class InMemoryCommandStore
             return state.agent();
         }
 
+        private SynchronizedState safeStore()
+        {
+            state.refreshRanges();
+            return state;
+        }
+
         @Override
         public Future<Void> execute(PreLoadContext context, Consumer<? super SafeCommandStore> consumer)
         {
-            return state.execute(context, consumer);
+            return safeStore().execute(context, consumer);
         }
 
         @Override
         public <T> Future<T> submit(PreLoadContext context, Function<? super SafeCommandStore, T> function)
         {
-            return state.submit(context, function);
+            return safeStore().submit(context, function);
         }
 
         @Override
         public <T> T executeSync(PreLoadContext context, Function<? super SafeCommandStore, T> function)
         {
-            return state.executeSync(context, function);
+            return safeStore().executeSync(context, function);
         }
 
         @Override
@@ -376,7 +388,7 @@ public class InMemoryCommandStore
 
         class AsyncState extends State implements SafeCommandStore
         {
-            public AsyncState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpoch rangesForEpoch, CommandStore commandStore)
+            public AsyncState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpochHolder rangesForEpoch, CommandStore commandStore)
             {
                 super(time, agent, store, progressLog, rangesForEpoch, commandStore);
             }
@@ -399,18 +411,18 @@ public class InMemoryCommandStore
         private final ExecutorService executor;
         private final AsyncState state;
 
-        public SingleThread(int id, int generation, int shardIndex, int numShards, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpoch rangesForEpoch)
+        public SingleThread(int id, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpochHolder rangesForEpoch)
         {
-            super(id, generation, shardIndex, numShards);
+            super(id);
             executor = Executors.newSingleThreadExecutor(r -> {
                 Thread thread = new Thread(r);
-                thread.setName(CommandStore.class.getSimpleName() + '[' + time.id() + ':' + shardIndex + ']');
+                thread.setName(CommandStore.class.getSimpleName() + '[' + time.id() + ']');
                 return thread;
             });
             state = newState(time, agent, store, progressLogFactory, rangesForEpoch);
         }
 
-        AsyncState newState(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpoch rangesForEpoch)
+        AsyncState newState(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpochHolder rangesForEpoch)
         {
             return new AsyncState(time, agent, store, progressLogFactory.create(this), rangesForEpoch, this);
         }
@@ -421,16 +433,22 @@ public class InMemoryCommandStore
             return state.agent();
         }
 
+        private State safeStore()
+        {
+            state.refreshRanges();
+            return state;
+        }
+
         @Override
         public Future<Void> execute(PreLoadContext context, Consumer<? super SafeCommandStore> consumer)
         {
-            return state.execute(context, consumer);
+            return safeStore().execute(context, consumer);
         }
 
         @Override
         public <T> Future<T> submit(PreLoadContext context, Function<? super SafeCommandStore, T> function)
         {
-            return state.submit(context, function);
+            return safeStore().submit(context, function);
         }
 
         @Override
@@ -444,7 +462,7 @@ public class InMemoryCommandStore
     {
         class DebugState extends AsyncState
         {
-            public DebugState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpoch rangesForEpoch, CommandStore commandStore)
+            public DebugState(NodeTimeService time, Agent agent, DataStore store, ProgressLog progressLog, RangesForEpochHolder rangesForEpoch, CommandStore commandStore)
             {
                 super(time, agent, store, progressLog, rangesForEpoch, commandStore);
             }
@@ -522,9 +540,9 @@ public class InMemoryCommandStore
 
         private final AtomicReference<Thread> expectedThread = new AtomicReference<>();
 
-        public Debug(int id, int generation, int shardIndex, int numShards, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpoch rangesForEpoch)
+        public Debug(int id, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpochHolder rangesForEpoch)
         {
-            super(id, generation, shardIndex, numShards, time, agent, store, progressLogFactory, rangesForEpoch);
+            super(id, time, agent, store, progressLogFactory, rangesForEpoch);
         }
 
         private void assertThread()
@@ -543,7 +561,7 @@ public class InMemoryCommandStore
         }
 
         @Override
-        AsyncState newState(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpoch rangesForEpoch)
+        AsyncState newState(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, RangesForEpochHolder rangesForEpoch)
         {
             return new DebugState(time, agent, store, progressLogFactory.create(this), rangesForEpoch, this);
         }
@@ -551,7 +569,7 @@ public class InMemoryCommandStore
 
     public static State inMemory(CommandStore unsafeStore)
     {
-        return (unsafeStore instanceof Synchronized) ? ((Synchronized) unsafeStore).state : ((SingleThread) unsafeStore).state;
+        return (unsafeStore instanceof Synchronized) ? ((Synchronized) unsafeStore).safeStore() : ((SingleThread) unsafeStore).safeStore();
     }
 
     public static State inMemory(SafeCommandStore safeStore)
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStores.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStores.java
index d7b726f..9ab4219 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStores.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStores.java
@@ -23,7 +23,6 @@ import accord.api.Agent;
 import accord.api.DataStore;
 import accord.api.ProgressLog;
 import accord.local.CommandStore;
-import accord.local.Node;
 import accord.primitives.Routables;
 import accord.utils.MapReduce;
 
@@ -34,9 +33,9 @@ public class InMemoryCommandStores
 {
     public static class Synchronized extends SyncCommandStores
     {
-        public Synchronized(int num, Node node, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory)
+        public Synchronized(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory)
         {
-            super(num, node, agent, store, progressLogFactory, InMemoryCommandStore.Synchronized::new);
+            super(time, agent, store, shardDistributor, progressLogFactory, InMemoryCommandStore.Synchronized::new);
         }
 
         public <T> T mapReduce(PreLoadContext context, Routables<?, ?> keys, long minEpoch, long maxEpoch, MapReduce<? super SafeCommandStore, T> map)
@@ -64,22 +63,22 @@ public class InMemoryCommandStores
 
     public static class SingleThread extends AsyncCommandStores
     {
-        public SingleThread(int num, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory)
+        public SingleThread(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory)
         {
-            super(num, time, agent, store, progressLogFactory, InMemoryCommandStore.SingleThread::new);
+            super(time, agent, store, shardDistributor, progressLogFactory, InMemoryCommandStore.SingleThread::new);
         }
 
-        public SingleThread(int num, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
+        public SingleThread(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
         {
-            super(num, time, agent, store, progressLogFactory, shardFactory);
+            super(time, agent, store, shardDistributor, progressLogFactory, shardFactory);
         }
     }
 
     public static class Debug extends InMemoryCommandStores.SingleThread
     {
-        public Debug(int num, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory)
+        public Debug(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory)
         {
-            super(num, time, agent, store, progressLogFactory, InMemoryCommandStore.Debug::new);
+            super(time, agent, store, shardDistributor, progressLogFactory, InMemoryCommandStore.Debug::new);
         }
     }
 }
diff --git a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
index afeb85e..c6069dc 100644
--- a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
+++ b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
@@ -103,7 +103,9 @@ public class SimpleProgressLog implements ProgressLog.Factory
 
                 void setProgress(Progress newProgress)
                 {
-                    this.progress = newProgress;
+                    Invariants.checkState(progress != Done);
+
+                    progress = newProgress;
                     switch (newProgress)
                     {
                         default: throw new AssertionError();
@@ -111,6 +113,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                         case Done:
                         case Investigating:
                             remove();
+                            Invariants.paranoid(isFree());
                             break;
                         case Expected:
                         case NoProgress:
@@ -129,8 +132,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                         case Investigating:
                             throw new IllegalStateException();
                         case Expected:
-                            if (isFree())
-                                throw new IllegalStateException();
+                            Invariants.paranoid(!isFree());
                             progress = NoProgress;
                             return false;
                         case NoProgress:
@@ -196,7 +198,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                         case Committed:
                         case ReadyToExecute:
                             status = CoordinateStatus.Done;
-                            setProgress(NoneExpected);
+                            setProgress(Done);
                         case Done:
                     }
                 }
@@ -223,9 +225,11 @@ public class SimpleProgressLog implements ProgressLog.Factory
                                 // if the home shard is at an earlier phase, it must run recovery
                                 long epoch = command.executeAt().epoch;
                                 node.withEpoch(epoch, () -> debugInvestigating = FetchData.fetch(PreApplied.minKnown, node, txnId, command.route(), epoch, (success, fail) -> {
-                                    // should have found enough information to apply the result, but in case we did not reset progress
-                                    if (progress() == Investigating)
-                                        setProgress(Expected);
+                                    commandStore.execute(PreLoadContext.empty(), ignore -> {
+                                        // should have found enough information to apply the result, but in case we did not reset progress
+                                        if (progress() == Investigating)
+                                            setProgress(Expected);
+                                    });
                                 }));
                             }
                             else
@@ -235,25 +239,27 @@ public class SimpleProgressLog implements ProgressLog.Factory
 
                                     Future<? extends Outcome> recover = node.maybeRecover(txnId, homeKey, command.route(), token);
                                     recover.addCallback((success, fail) -> {
-                                        if (status.isAtMostReadyToExecute() && progress() == Investigating)
-                                        {
-                                            setProgress(Expected);
-                                            if (fail != null)
-                                                return;
-
-                                            ProgressToken token = success.asProgressToken();
-                                            // TODO: avoid returning null (need to change semantics here in this case, though, as Recover doesn't return CheckStatusOk)
-                                            if (token.durability.isDurable())
+                                        commandStore.execute(PreLoadContext.empty(), ignore -> {
+                                            if (status.isAtMostReadyToExecute() && progress() == Investigating)
                                             {
-                                                commandStore.execute(contextFor(txnId), safeStore -> {
-                                                    Command cmd = safeStore.command(txnId);
-                                                    cmd.setDurability(safeStore, token.durability, homeKey, null);
-                                                    safeStore.progressLog().durable(txnId, cmd.maxUnseekables(), null);
-                                                }).addCallback(commandStore.agent());
+                                                setProgress(Expected);
+                                                if (fail != null)
+                                                    return;
+
+                                                ProgressToken token = success.asProgressToken();
+                                                // TODO: avoid returning null (need to change semantics here in this case, though, as Recover doesn't return CheckStatusOk)
+                                                if (token.durability.isDurable())
+                                                {
+                                                    commandStore.execute(contextFor(txnId), safeStore -> {
+                                                        Command cmd = safeStore.command(txnId);
+                                                        cmd.setDurability(safeStore, token.durability, homeKey, null);
+                                                        safeStore.progressLog().durable(txnId, cmd.maxUnseekables(), null);
+                                                    }).addCallback(commandStore.agent());
+                                                }
+
+                                                updateMax(token);
                                             }
-
-                                            updateMax(token);
-                                        }
+                                        });
                                     });
 
                                     debugInvestigating = recover;
@@ -280,6 +286,9 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     {
                         // TODO: callbacks should be associated with a commandStore for processing to avoid this
                         commandStore.execute(PreLoadContext.empty(), ignore -> {
+                            if (progress() == Done)
+                                return;
+
                             notAwareOfDurability.remove(from);
                             maybeDone();
                         });
@@ -358,7 +367,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
 
                 private void maybeDone()
                 {
-                    if (notAwareOfDurability.isEmpty())
+                    if (progress() != Done && notAwareOfDurability.isEmpty())
                     {
                         status = DisseminateStatus.Done;
                         setProgress(Done);
@@ -479,15 +488,17 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     Unseekables<?, ?> someKeys = unseekables(command);
 
                     BiConsumer<Known, Throwable> callback = (success, fail) -> {
-                        if (progress() != Investigating)
-                            return;
+                        commandStore.execute(PreLoadContext.empty(), ignore -> {
+                            if (progress() != Investigating)
+                                return;
 
-                        setProgress(Expected);
-                        if (fail == null)
-                        {
-                            if (!success.isDefinitionKnown()) invalidate(node, txnId, someKeys);
-                            else record(success);
-                        }
+                            setProgress(Expected);
+                            if (fail == null)
+                            {
+                                if (!success.isDefinitionKnown()) invalidate(node, txnId, someKeys);
+                                else record(success);
+                            }
+                        });
                     };
 
                     node.withEpoch(toEpoch, () -> {
@@ -507,12 +518,14 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     RoutingKey someKey = Route.isRoute(someKeys) ? (Route.castToRoute(someKeys)).homeKey() : someKeys.get(0).someIntersectingRoutingKey();
                     someKeys = someKeys.with(someKey);
                     debugInvestigating = Invalidate.invalidate(node, txnId, someKeys, (success, fail) -> {
-                        if (progress() != Investigating)
-                            return;
+                        commandStore.execute(PreLoadContext.empty(), ignore -> {
+                            if (progress() != Investigating)
+                                return;
 
-                        setProgress(Expected);
-                        if (fail == null && success.asProgressToken().durability.isDurable())
-                            setProgress(Done);
+                            setProgress(Expected);
+                            if (fail == null && success.asProgressToken().durability.isDurable())
+                                setProgress(Done);
+                        });
                     });
                 }
 
@@ -532,7 +545,8 @@ public class SimpleProgressLog implements ProgressLog.Factory
 
                 void setSafe()
                 {
-                    setProgress(Done);
+                    if (progress() != Done)
+                        setProgress(Done);
                 }
 
                 @Override
@@ -541,10 +555,12 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     // make sure a quorum of the home shard is aware of the transaction, so we can rely on it to ensure progress
                     Future<Void> inform = inform(node, txnId, command.homeKey());
                     inform.addCallback((success, fail) -> {
-                        if (progress() == Done)
-                            return;
+                        commandStore.execute(PreLoadContext.empty(), ignore -> {
+                            if (progress() == Done)
+                                return;
 
-                        setProgress(fail != null ? Expected : Done);
+                            setProgress(fail != null ? Expected : Done);
+                        });
                     });
                 }
 
@@ -809,7 +825,8 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     if (run.shouldRun())
                     {
                         commandStore.execute(contextFor(run.txnId()), safeStore -> {
-                            run.run(safeStore.command(run.txnId()));
+                            if (run.shouldRun()) // could have been completed by a callback
+                                run.run(safeStore.command(run.txnId()));
                         });
                     }
                 }
diff --git a/accord-core/src/main/java/accord/local/AsyncCommandStores.java b/accord-core/src/main/java/accord/local/AsyncCommandStores.java
index 6c0a6dd..f586ba7 100644
--- a/accord-core/src/main/java/accord/local/AsyncCommandStores.java
+++ b/accord-core/src/main/java/accord/local/AsyncCommandStores.java
@@ -55,9 +55,9 @@ public class AsyncCommandStores extends CommandStores<CommandStore>
         }
     }
 
-    public AsyncCommandStores(int num, NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
+    public AsyncCommandStores(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
     {
-        super(num, time, agent, store, progressLogFactory, shardFactory);
+        super(time, agent, store, shardDistributor, progressLogFactory, shardFactory);
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/local/Command.java b/accord-core/src/main/java/accord/local/Command.java
index 7b8cacb..592c90f 100644
--- a/accord-core/src/main/java/accord/local/Command.java
+++ b/accord-core/src/main/java/accord/local/Command.java
@@ -416,7 +416,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
     {
         Ranges ranges = safeStore.ranges().since(executeAt().epoch);
         if (ranges != null) {
-            partialDeps().forEachOn(ranges, safeStore.commandStore()::hashIntersects, txnId -> {
+            partialDeps().forEachOn(ranges, txnId -> {
                 Command command = safeStore.ifLoaded(txnId);
                 if (command == null)
                 {
@@ -618,7 +618,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
                 break;
 
             case PreApplied:
-                if (executeRanges(safeStore, executeAt()).intersects(writes().keys, safeStore.commandStore()::hashIntersects))
+                if (executeRanges(safeStore, executeAt()).intersects(writes().keys))
                 {
                     logger.trace("{}: applying", txnId());
                     apply(safeStore);
@@ -835,6 +835,8 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (homeKey() == null)
         {
             setHomeKey(homeKey);
+            // TODO (low priority, safety): if we're processed on a node that does not know the latest epoch,
+            //      do we guarantee the home key calculation is unchanged since the prior epoch?
             if (progressKey() == null && owns(safeStore, txnId().epoch, homeKey))
                 progressKey(homeKey);
         }
@@ -862,9 +864,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (!coordinateRanges.contains(progressKey))
             return No;
 
-        if (!safeStore.commandStore().hashIntersects(progressKey))
-            return No;
-
         return progressKey.equals(homeKey()) ? Home : Local;
     }
 
@@ -904,9 +903,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (!coordinateRanges.contains(progressKey))
             return No;
 
-        if (!safeStore.commandStore().hashIntersects(progressKey))
-            return No;
-
         return progressKey.equals(homeKey()) ? Home : Local;
     }
 
@@ -1164,9 +1160,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
      */
     public boolean owns(SafeCommandStore safeStore, long epoch, RoutingKey someKey)
     {
-        if (!safeStore.commandStore().hashIntersects(someKey))
-            return false;
-
         return safeStore.ranges().at(epoch).contains(someKey);
     }
 
@@ -1213,11 +1206,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
             throw new UnsupportedOperationException();
         }
 
-        @Override
-        public int routingHash()
-        {
-            throw new UnsupportedOperationException();
-        }
     }
 
     private static final NoProgressKey NO_PROGRESS_KEY = new NoProgressKey();
diff --git a/accord-core/src/main/java/accord/local/CommandStore.java b/accord-core/src/main/java/accord/local/CommandStore.java
index d28d618..04addde 100644
--- a/accord-core/src/main/java/accord/local/CommandStore.java
+++ b/accord-core/src/main/java/accord/local/CommandStore.java
@@ -19,17 +19,14 @@
 package accord.local;
 
 import accord.api.*;
-import accord.local.CommandStores.ShardedRanges;
 import accord.api.ProgressLog;
-import accord.primitives.*;
 import accord.api.DataStore;
+import accord.local.CommandStores.RangesForEpochHolder;
 import org.apache.cassandra.utils.concurrent.Future;
 
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import static accord.utils.Invariants.checkArgument;
-
 /**
  * Single threaded internal shard of accord transaction metadata
  */
@@ -38,38 +35,18 @@ public abstract class CommandStore
     public interface Factory
     {
         CommandStore create(int id,
-                            int generation,
-                            int shardIndex,
-                            int numShards,
                             NodeTimeService time,
                             Agent agent,
                             DataStore store,
                             ProgressLog.Factory progressLogFactory,
-                            RangesForEpoch rangesForEpoch);
-    }
-
-    public interface RangesForEpoch
-    {
-        Ranges at(long epoch);
-        Ranges between(long fromInclusive, long toInclusive);
-        Ranges since(long epoch);
-        boolean owns(long epoch, RoutingKey key);
+                            RangesForEpochHolder rangesForEpoch);
     }
 
     private final int id; // unique id
-    private final int generation;
-    private final int shardIndex;
-    private final int numShards;
 
-    public CommandStore(int id,
-                        int generation,
-                        int shardIndex,
-                        int numShards)
+    public CommandStore(int id)
     {
         this.id = id;
-        this.generation = generation;
-        this.shardIndex = checkArgument(shardIndex, shardIndex < numShards);
-        this.numShards = numShards;
     }
 
     public int id()
@@ -77,28 +54,6 @@ public abstract class CommandStore
         return id;
     }
 
-    // TODO (now): rename to shardIndex
-    public int index()
-    {
-        return shardIndex;
-    }
-
-    // TODO (now): rename to shardGeneration
-    public int generation()
-    {
-        return generation;
-    }
-
-    public boolean hashIntersects(RoutableKey key)
-    {
-        return ShardedRanges.keyIndex(key, numShards) == shardIndex;
-    }
-
-    public boolean hashIntersects(Routable routable)
-    {
-        return routable instanceof Range || hashIntersects((RoutableKey) routable);
-    }
-
     public abstract Agent agent();
     public abstract Future<Void> execute(PreLoadContext context, Consumer<? super SafeCommandStore> consumer);
     public abstract <T> Future<T> submit(PreLoadContext context, Function<? super SafeCommandStore, T> apply);
diff --git a/accord-core/src/main/java/accord/local/CommandStores.java b/accord-core/src/main/java/accord/local/CommandStores.java
index 87d39b2..92b2271 100644
--- a/accord-core/src/main/java/accord/local/CommandStores.java
+++ b/accord-core/src/main/java/accord/local/CommandStores.java
@@ -19,7 +19,6 @@
 package accord.local;
 
 import accord.api.*;
-import accord.local.CommandStore.RangesForEpoch;
 import accord.primitives.*;
 import accord.api.RoutingKey;
 import accord.topology.Topology;
@@ -27,17 +26,19 @@ import accord.utils.MapReduce;
 import accord.utils.MapReduceConsume;
 
 import accord.utils.ReducingFuture;
+import com.carrotsearch.hppc.IntObjectMap;
+import com.carrotsearch.hppc.IntObjectScatterMap;
 import com.google.common.annotations.VisibleForTesting;
 import org.apache.cassandra.utils.concurrent.Future;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.stream.IntStream;
 
 import static accord.local.PreLoadContext.empty;
-
 import static accord.utils.Invariants.checkArgument;
 
 /**
@@ -47,10 +48,10 @@ public abstract class CommandStores<S extends CommandStore>
 {
     public interface Factory
     {
-        CommandStores<?> create(int num,
-                                Node node,
+        CommandStores<?> create(NodeTimeService time,
                                 Agent agent,
                                 DataStore store,
+                                ShardDistributor shardDistributor,
                                 ProgressLog.Factory progressLogFactory);
     }
 
@@ -61,63 +62,79 @@ public abstract class CommandStores<S extends CommandStore>
         private final DataStore store;
         private final ProgressLog.Factory progressLogFactory;
         private final CommandStore.Factory shardFactory;
-        private final int numShards;
 
-        Supplier(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory, int numShards)
+        Supplier(NodeTimeService time, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
         {
             this.time = time;
             this.agent = agent;
             this.store = store;
             this.progressLogFactory = progressLogFactory;
             this.shardFactory = shardFactory;
-            this.numShards = numShards;
         }
 
-        CommandStore create(int id, int generation, int shardIndex, RangesForEpoch rangesForEpoch)
+        CommandStore create(int id, RangesForEpochHolder rangesForEpoch)
         {
-            return shardFactory.create(id, generation, shardIndex, numShards, time, agent, store, progressLogFactory, rangesForEpoch);
+            return shardFactory.create(id, time, agent, store, progressLogFactory, rangesForEpoch);
         }
+    }
+
+    public static class RangesForEpochHolder
+    {
+        // no need for safe publication; RangesForEpoch members are final, and will be guarded by other synchronization actions
+        protected RangesForEpoch current;
+
+        /**
+         * This is updated asynchronously, so should only be fetched between executing tasks;
+         * otherwise the contents may differ between invocations for the same task
+         * @return the current RangesForEpoch
+         */
+        public RangesForEpoch get() { return current; }
+    }
+
+    static class ShardHolder
+    {
+        final CommandStore store;
+        final RangesForEpochHolder ranges;
 
-        ShardedRanges createShardedRanges(int generation, long epoch, Ranges ranges, RangesForEpoch rangesForEpoch)
+        ShardHolder(CommandStore store, RangesForEpochHolder ranges)
         {
-            CommandStore[] newStores = new CommandStore[numShards];
-            for (int i=0; i<numShards; i++)
-                newStores[i] = create(generation * numShards + i, generation, i, rangesForEpoch);
+            this.store = store;
+            this.ranges = ranges;
+        }
 
-            return new ShardedRanges(newStores, epoch, ranges);
+        RangesForEpoch ranges()
+        {
+            return ranges.current;
         }
     }
 
-    protected static class ShardedRanges
+    public static class RangesForEpoch
     {
-        final CommandStore[] shards;
         final long[] epochs;
         final Ranges[] ranges;
 
-        protected ShardedRanges(CommandStore[] shards, long epoch, Ranges ranges)
+        public RangesForEpoch(long epoch, Ranges ranges)
         {
-            this.shards = checkArgument(shards, shards.length <= 64);
             this.epochs = new long[] { epoch };
             this.ranges = new Ranges[] { ranges };
         }
 
-        private ShardedRanges(CommandStore[] shards, long[] epochs, Ranges[] ranges)
+        public RangesForEpoch(long[] epochs, Ranges[] ranges)
         {
-            this.shards = checkArgument(shards, shards.length <= 64);
             this.epochs = epochs;
             this.ranges = ranges;
         }
 
-        ShardedRanges withRanges(long epoch, Ranges ranges)
+        public RangesForEpoch withRanges(long epoch, Ranges ranges)
         {
             long[] newEpochs = Arrays.copyOf(this.epochs, this.epochs.length + 1);
             Ranges[] newRanges = Arrays.copyOf(this.ranges, this.ranges.length + 1);
             newEpochs[this.epochs.length] = epoch;
             newRanges[this.ranges.length] = ranges;
-            return new ShardedRanges(shards, newEpochs, newRanges);
+            return new RangesForEpoch(newEpochs, newRanges);
         }
 
-        Ranges rangesForEpoch(long epoch)
+        public Ranges at(long epoch)
         {
             int i = Arrays.binarySearch(epochs, epoch);
             if (i < 0) i = -2 -i;
@@ -125,13 +142,13 @@ public abstract class CommandStores<S extends CommandStore>
             return ranges[i];
         }
 
-        Ranges rangesBetweenEpochs(long fromInclusive, long toInclusive)
+        public Ranges between(long fromInclusive, long toInclusive)
         {
             if (fromInclusive > toInclusive)
                 throw new IndexOutOfBoundsException();
 
             if (fromInclusive == toInclusive)
-                return rangesForEpoch(fromInclusive);
+                return at(fromInclusive);
 
             int i = Arrays.binarySearch(epochs, fromInclusive);
             if (i < 0) i = -2 - i;
@@ -147,7 +164,7 @@ public abstract class CommandStores<S extends CommandStore>
             return result;
         }
 
-        Ranges rangesSinceEpoch(long epoch)
+        public Ranges since(long epoch)
         {
             int i = Arrays.binarySearch(epochs, epoch);
             if (i < 0) i = Math.max(0, -2 -i);
@@ -164,88 +181,56 @@ public abstract class CommandStores<S extends CommandStore>
             return i;
         }
 
-        public long all()
-        {
-            return -1L >>> (64 - shards.length);
-        }
-
-        public <T extends Routable> long shards(Routables<T, ?> keysOrRanges, long minEpoch, long maxEpoch)
+        public boolean intersects(long epoch, AbstractKeys<?, ?> keys)
         {
-            long terminalValue = -1L >>> (32 - shards.length);
-            switch (keysOrRanges.kindOfContents())
-            {
-                default: throw new AssertionError();
-                case Key:
-                {
-                    long accumulate = 0L;
-                    for (int i = Math.max(0, indexForEpoch(minEpoch)), maxi = indexForEpoch(maxEpoch); i <= maxi ; ++i)
-                    {
-                        accumulate = Routables.foldl((AbstractKeys<?, ?>)keysOrRanges, ranges[i], ShardedRanges::addKeyIndex, shards.length, accumulate, terminalValue);
-                    }
-                    return accumulate;
-                }
-
-                case Range:
-                {
-                    long accumulate = 0L;
-                    for (int i = Math.max(0, indexForEpoch(minEpoch)), maxi = indexForEpoch(maxEpoch); i <= maxi ; ++i)
-                    {
-                        // include every shard if we match a range
-                        accumulate = Routables.foldl((Ranges)keysOrRanges, ranges[i], (k, p, a, idx) -> p, terminalValue, accumulate, terminalValue);
-                    }
-                    return accumulate;
-                }
-            }
+            return at(epoch).intersects(keys);
         }
 
-        Ranges currentRanges()
+        public Ranges currentRanges()
         {
             return ranges[ranges.length - 1];
         }
 
-        static long keyIndex(RoutableKey key, long numShards)
+        public Ranges maximalRanges()
         {
-            return Integer.toUnsignedLong(key.routingHash()) % numShards;
-        }
-
-        private static long addKeyIndex(RoutableKey key, long numShards, long accumulate, int i)
-        {
-            return accumulate | (1L << keyIndex(key, numShards));
+            return ranges[0];
         }
     }
 
     static class Snapshot
     {
-        final ShardedRanges[] ranges;
-        final Topology global;
+        final ShardHolder[] shards;
+        final IntObjectMap<CommandStore> byId;
         final Topology local;
-        final int size;
+        final Topology global;
 
-        Snapshot(ShardedRanges[] ranges, Topology global, Topology local)
+        Snapshot(ShardHolder[] shards, Topology local, Topology global)
         {
-            this.ranges = ranges;
-            this.global = global;
+            this.shards = shards;
+            this.byId = new IntObjectScatterMap<>(shards.length);
+            for (ShardHolder shard : shards)
+                byId.put(shard.store.id(), shard.store);
             this.local = local;
-            int size = 0;
-            for (ShardedRanges group : ranges)
-                size += group.shards.length;
-            this.size = size;
+            this.global = global;
         }
     }
 
     final Supplier supplier;
+    final ShardDistributor shardDistributor;
     volatile Snapshot current;
+    int nextId;
 
-    private CommandStores(Supplier supplier)
+    private CommandStores(Supplier supplier, ShardDistributor shardDistributor)
     {
         this.supplier = supplier;
-        this.current = new Snapshot(new ShardedRanges[0], Topology.EMPTY, Topology.EMPTY);
+        this.shardDistributor = shardDistributor;
+        this.current = new Snapshot(new ShardHolder[0], Topology.EMPTY, Topology.EMPTY);
     }
 
-    public CommandStores(int num, NodeTimeService time, Agent agent, DataStore store,
+    public CommandStores(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor,
                          ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
     {
-        this(new Supplier(time, agent, store, progressLogFactory, shardFactory, num));
+        this(new Supplier(time, agent, store, progressLogFactory, shardFactory), shardDistributor);
     }
 
     public Topology local()
@@ -258,7 +243,7 @@ public abstract class CommandStores<S extends CommandStore>
         return current.global;
     }
 
-    private Snapshot updateTopology(Snapshot prev, Topology newTopology)
+    private synchronized Snapshot updateTopology(Snapshot prev, Topology newTopology)
     {
         checkArgument(!newTopology.isSubset(), "Use full topology for CommandStores.updateTopology");
 
@@ -269,70 +254,37 @@ public abstract class CommandStores<S extends CommandStore>
         Topology newLocalTopology = newTopology.forNode(supplier.time.id()).trim();
         Ranges added = newLocalTopology.ranges().difference(prev.local.ranges());
         Ranges subtracted = prev.local.ranges().difference(newLocalTopology.ranges());
-//            for (ShardedRanges range : stores.ranges)
-//            {
-//                // FIXME: remove this (and the corresponding check in TopologyRandomizer) once lower bounds are implemented.
-//                //  In the meantime, the logic needed to support acquiring ranges that we previously replicated is pretty
-//                //  convoluted without the ability to jettison epochs.
-//                Invariants.checkState(!range.ranges.intersects(added));
-//            }
 
         if (added.isEmpty() && subtracted.isEmpty())
-            return new Snapshot(prev.ranges, newTopology, newLocalTopology);
+            return new Snapshot(prev.shards, newLocalTopology, newTopology);
 
-        ShardedRanges[] result = new ShardedRanges[prev.ranges.length + (added.isEmpty() ? 0 : 1)];
+        List<ShardHolder> result = new ArrayList<>(prev.shards.length + added.size());
         if (subtracted.isEmpty())
         {
-            int newGeneration = prev.ranges.length;
-            System.arraycopy(prev.ranges, 0, result, 0, newGeneration);
-            result[newGeneration] = supplier.createShardedRanges(newGeneration, epoch, added, rangesForEpochFunction(newGeneration));
+            Collections.addAll(result, prev.shards);
         }
         else
         {
-            int i = 0;
-            while (i < prev.ranges.length)
+            for (ShardHolder shard : prev.shards)
             {
-                ShardedRanges ranges = prev.ranges[i];
-                if (ranges.currentRanges().intersects(subtracted))
-                    ranges = ranges.withRanges(newTopology.epoch(), ranges.currentRanges().difference(subtracted));
-                result[i++] = ranges;
+                if (subtracted.intersects(shard.ranges().currentRanges()))
+                    shard.ranges.current = shard.ranges().withRanges(newTopology.epoch(), shard.ranges().currentRanges().difference(subtracted));
+                result.add(shard);
             }
-            if (i < result.length)
-                result[i] = supplier.createShardedRanges(i, epoch, added, rangesForEpochFunction(i));
         }
 
-        return new Snapshot(result, newTopology, newLocalTopology);
-    }
-
-    private RangesForEpoch rangesForEpochFunction(int generation)
-    {
-        return new RangesForEpoch()
+        if (!added.isEmpty())
         {
-            @Override
-            public Ranges at(long epoch)
+            // TODO (required): shards must rebalance
+            for (Ranges add : shardDistributor.split(added))
             {
-                return current.ranges[generation].rangesForEpoch(epoch);
-            }
-
-            @Override
-            public Ranges between(long fromInclusive, long toInclusive)
-            {
-                return current.ranges[generation].rangesBetweenEpochs(fromInclusive, toInclusive);
-            }
-
-            @Override
-            public Ranges since(long epoch)
-            {
-                return current.ranges[generation].rangesSinceEpoch(epoch);
-            }
-
-            @Override
-            public boolean owns(long epoch, RoutingKey key)
-            {
-                return current.ranges[generation].rangesForEpoch(epoch).contains(key);
+                RangesForEpochHolder rangesHolder = new RangesForEpochHolder();
+                rangesHolder.current = new RangesForEpoch(epoch, add);
+                result.add(new ShardHolder(supplier.create(nextId++, rangesHolder), rangesHolder));
             }
+        }
 
-        };
+        return new Snapshot(result.toArray(new ShardHolder[0]), newLocalTopology, newTopology);
     }
 
     interface MapReduceAdapter<S extends CommandStore, Intermediate, Accumulator, O>
@@ -348,12 +300,9 @@ public abstract class CommandStores<S extends CommandStore>
     {
         List<Future<Void>> list = new ArrayList<>();
         Snapshot snapshot = current;
-        for (ShardedRanges ranges : snapshot.ranges)
+        for (ShardHolder shard : snapshot.shards)
         {
-            for (CommandStore store : ranges.shards)
-            {
-                list.add(store.execute(empty(), forEach));
-            }
+            list.add(shard.store.execute(empty(), forEach));
         }
         return ReducingFuture.reduce(list, (a, b) -> null);
     }
@@ -433,16 +382,17 @@ public abstract class CommandStores<S extends CommandStore>
                                        MapReduceAdapter<? super S, T1, T2, O> adapter)
     {
         T2 accumulator = adapter.allocate();
-        for (ShardedRanges ranges : current.ranges)
+        Snapshot snapshot = current;
+        ShardHolder[] shards = snapshot.shards;
+        for (ShardHolder shard : shards)
         {
-            long bits = ranges.shards(keys, minEpoch, maxEpoch);
-            while (bits != 0)
-            {
-                int i = Long.numberOfTrailingZeros(bits);
-                T1 next = adapter.apply(mapReduce, (S)ranges.shards[i], context);
-                accumulator = adapter.reduce(mapReduce, accumulator, next);
-                bits ^= Long.lowestOneBit(bits);
-            }
+            // TODO (urgent, efficiency): range map for intersecting ranges (e.g. that to be introduced for range dependencies)
+            Ranges shardRanges = shard.ranges().between(minEpoch, maxEpoch);
+            if (!shardRanges.intersects(keys))
+                continue;
+
+            T1 next = adapter.apply(mapReduce, (S)shard.store, context);
+            accumulator = adapter.reduce(mapReduce, accumulator, next);
         }
         return adapter.reduce(mapReduce, accumulator);
     }
@@ -450,7 +400,7 @@ public abstract class CommandStores<S extends CommandStore>
     protected <T1, T2, O> T1 mapReduce(PreLoadContext context, IntStream commandStoreIds, MapReduce<? super SafeCommandStore, O> mapReduce,
                                        MapReduceAdapter<? super S, T1, T2, O> adapter)
     {
-        // TODO: efficiency
+        // TODO (low priority, efficiency): avoid using an array, or use a scratch buffer
         int[] ids = commandStoreIds.toArray();
         T2 accumulator = adapter.allocate();
         for (int id : ids)
@@ -468,33 +418,30 @@ public abstract class CommandStores<S extends CommandStore>
 
     public synchronized void shutdown()
     {
-        for (ShardedRanges group : current.ranges)
-            for (CommandStore commandStore : group.shards)
-                commandStore.shutdown();
+        for (ShardHolder shard : current.shards)
+            shard.store.shutdown();
     }
 
-    CommandStore forId(int id)
+    public CommandStore forId(int id)
     {
-        ShardedRanges[] ranges = current.ranges;
-        return ranges[id / supplier.numShards].shards[id % supplier.numShards];
+        Snapshot snapshot = current;
+        return snapshot.byId.get(id);
+    }
+
+    public int count()
+    {
+        return current.shards.length;
     }
 
     @VisibleForTesting
     public CommandStore unsafeForKey(Key key)
     {
-        ShardedRanges[] ranges = current.ranges;
-        for (ShardedRanges group : ranges)
+        ShardHolder[] shards = current.shards;
+        for (ShardHolder shard : shards)
         {
-            if (group.currentRanges().contains(key))
-            {
-                for (CommandStore store : group.shards)
-                {
-                    if (store.hashIntersects(key))
-                        return store;
-                }
-            }
+            if (shard.ranges().currentRanges().contains(key))
+                return shard.store;
         }
         throw new IllegalArgumentException();
     }
-
 }
diff --git a/accord-core/src/main/java/accord/local/Node.java b/accord-core/src/main/java/accord/local/Node.java
index 4e52b67..e348a76 100644
--- a/accord-core/src/main/java/accord/local/Node.java
+++ b/accord-core/src/main/java/accord/local/Node.java
@@ -127,7 +127,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
     private final Map<TxnId, Future<? extends Outcome>> coordinating = new ConcurrentHashMap<>();
 
     public Node(Id id, MessageSink messageSink, ConfigurationService configService, LongSupplier nowSupplier,
-                Supplier<DataStore> dataSupplier, Agent agent, Random random, Scheduler scheduler, TopologySorter.Supplier topologySorter,
+                Supplier<DataStore> dataSupplier, ShardDistributor shardDistributor, Agent agent, Random random, Scheduler scheduler, TopologySorter.Supplier topologySorter,
                 Function<Node, ProgressLog.Factory> progressLogFactory, CommandStores.Factory factory)
     {
         this.id = id;
@@ -140,7 +140,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         this.agent = agent;
         this.random = random;
         this.scheduler = scheduler;
-        this.commandStores = factory.create(numCommandShards(), this, agent, dataSupplier.get(), progressLogFactory.apply(this));
+        this.commandStores = factory.create(this, agent, dataSupplier.get(), shardDistributor, progressLogFactory.apply(this));
 
         configService.registerListener(this);
         onTopologyUpdate(topology, false);
@@ -530,7 +530,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public CommandStore unsafeByIndex(int index)
     {
-        return commandStores.current.ranges[0].shards[index];
+        return commandStores.current.shards[index].store;
     }
 
     public LongSupplier unsafeGetNowSupplier()
diff --git a/accord-core/src/main/java/accord/local/SafeCommandStore.java b/accord-core/src/main/java/accord/local/SafeCommandStore.java
index 649837a..04ace5c 100644
--- a/accord-core/src/main/java/accord/local/SafeCommandStore.java
+++ b/accord-core/src/main/java/accord/local/SafeCommandStore.java
@@ -67,7 +67,7 @@ public interface SafeCommandStore
     Agent agent();
     ProgressLog progressLog();
     NodeTimeService time();
-    CommandStore.RangesForEpoch ranges();
+    CommandStores.RangesForEpoch ranges();
     long latestEpoch();
     Timestamp preaccept(TxnId txnId, Seekables<?, ?> keys);
 
diff --git a/accord-core/src/main/java/accord/local/ShardDistributor.java b/accord-core/src/main/java/accord/local/ShardDistributor.java
new file mode 100644
index 0000000..d834929
--- /dev/null
+++ b/accord-core/src/main/java/accord/local/ShardDistributor.java
@@ -0,0 +1,95 @@
+package accord.local;
+
+import accord.primitives.Range;
+import accord.primitives.Ranges;
+import accord.utils.Invariants;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+public interface ShardDistributor
+{
+    // TODO: this is overly simplistic: need to supply existing distribution,
+    //  and support gradual local redistribution to keep number of shards eventually the same
+    List<Ranges> split(Ranges ranges);
+
+    class EvenSplit<T> implements ShardDistributor
+    {
+        public interface Splitter<T>
+        {
+            T sizeOf(Range range);
+            Range subRange(Range range, T start, T end);
+
+            T zero();
+            T add(T a, T b);
+            T subtract(T a, T b);
+            T divide(T a, int i);
+            T multiply(T a, int i);
+            int min(T v, int i);
+            int compare(T a, T b);
+        }
+
+        final int numberOfShards;
+        final Function<Ranges, ? extends Splitter<T>> splitter;
+
+        public EvenSplit(int numberOfShards, Function<Ranges, ? extends Splitter<T>> splitter)
+        {
+            this.numberOfShards = numberOfShards;
+            this.splitter = splitter;
+        }
+
+        @Override
+        public List<Ranges> split(Ranges ranges)
+        {
+            if (ranges.isEmpty())
+                return Collections.emptyList();
+
+            Splitter<T> splitter = this.splitter.apply(ranges);
+            T totalSize = splitter.zero();
+            for (int i = 0 ; i < ranges.size() ; ++i)
+                totalSize = splitter.add(totalSize, splitter.sizeOf(ranges.get(i)));
+
+            if (splitter.compare(totalSize, splitter.zero()) <= 0)
+                throw new IllegalStateException();
+
+            int numberOfShards = splitter.min(totalSize, this.numberOfShards);
+            T shardSize = splitter.divide(totalSize, numberOfShards);
+            List<Range> buffer = new ArrayList<>(ranges.size());
+            List<Ranges> result = new ArrayList<>(numberOfShards);
+            int ri = 0;
+            T rOffset = splitter.zero();
+            T rSize = splitter.sizeOf(ranges.get(0));
+            while (result.size() < numberOfShards)
+            {
+                T required = result.size() < numberOfShards - 1 ? shardSize : splitter.subtract(totalSize, splitter.multiply(shardSize, (numberOfShards - 1)));
+                while (true)
+                {
+                    if (splitter.compare(splitter.subtract(rSize, rOffset), required) >= 0)
+                    {
+                        buffer.add(splitter.subRange(ranges.get(ri), rOffset, splitter.add(rOffset, required)));
+                        result.add(Ranges.ofSortedAndDeoverlapped(buffer.toArray(new Range[0])));
+                        buffer.clear();
+                        rOffset = splitter.add(rOffset, required);
+                        if (splitter.compare(rOffset, rSize) >= 0 && ++ri < ranges.size())
+                        {
+                            Invariants.checkState(splitter.compare(rOffset, rSize) == 0);
+                            rOffset = splitter.zero();
+                            rSize = splitter.sizeOf(ranges.get(ri));
+                        }
+                        break;
+                    }
+                    else
+                    {
+                        buffer.add(splitter.subRange(ranges.get(ri), rOffset, rSize));
+                        required = splitter.subtract(required, splitter.subtract(rSize, rOffset));
+                        rOffset = splitter.zero();
+                        rSize = splitter.sizeOf(ranges.get(++ri));
+                    }
+                }
+            }
+            return result;
+        }
+    }
+}
diff --git a/accord-core/src/main/java/accord/local/SyncCommandStores.java b/accord-core/src/main/java/accord/local/SyncCommandStores.java
index 61b5de7..fde0fe4 100644
--- a/accord-core/src/main/java/accord/local/SyncCommandStores.java
+++ b/accord-core/src/main/java/accord/local/SyncCommandStores.java
@@ -19,16 +19,16 @@ public class SyncCommandStores extends CommandStores<SyncCommandStores.SyncComma
 
     public static abstract class SyncCommandStore extends CommandStore
     {
-        public SyncCommandStore(int id, int generation, int shardIndex, int numShards)
+        public SyncCommandStore(int id)
         {
-            super(id, generation, shardIndex, numShards);
+            super(id);
         }
         protected abstract <T> T executeSync(PreLoadContext context, Function<? super SafeCommandStore, T> function);
     }
 
-    public SyncCommandStores(int num, Node node, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
+    public SyncCommandStores(NodeTimeService time, Agent agent, DataStore store, ShardDistributor shardDistributor, ProgressLog.Factory progressLogFactory, CommandStore.Factory shardFactory)
     {
-        super(num, node, agent, store, progressLogFactory, shardFactory);
+        super(time, agent, store, shardDistributor, progressLogFactory, shardFactory);
     }
 
     protected static class SyncMapReduceAdapter<O> implements MapReduceAdapter<SyncCommandStore, O, O, O>
diff --git a/accord-core/src/main/java/accord/messages/ReadData.java b/accord-core/src/main/java/accord/messages/ReadData.java
index f6faab8..e003bca 100644
--- a/accord-core/src/main/java/accord/messages/ReadData.java
+++ b/accord-core/src/main/java/accord/messages/ReadData.java
@@ -146,8 +146,6 @@ public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements
             default:
                 throw new AssertionError();
             case Committed:
-                if (!Iterables.any(command.partialTxn().keys(), safeStore.commandStore()::hashIntersects))
-                    throw new IllegalStateException();
             case NotWitnessed:
             case PreAccepted:
             case Accepted:
diff --git a/accord-core/src/main/java/accord/primitives/AbstractKeys.java b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
index a089823..6886848 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractKeys.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
@@ -165,20 +165,12 @@ public abstract class AbstractKeys<K extends RoutableKey, KS extends Routables<K
         return !any(predicate);
     }
 
-    /**
-     * Count the number of keys matching the predicate and intersecting with the given ranges.
-     * If terminateAfter is greater than 0, the method will return once terminateAfter matches are encountered
-     */
     @Inline
     public final <V> V foldl(Ranges rs, IndexedFold<? super K, V> fold, V accumulator)
     {
         return Routables.foldl(this, rs, fold, accumulator);
     }
 
-    /**
-     * Count the number of keys matching the predicate and intersecting with the given ranges.
-     * If terminateAfter is greater than 0, the method will return once terminateAfter matches are encountered
-     */
     @Inline
     public final void forEach(Ranges rs, Consumer<? super K> forEach)
     {
diff --git a/accord-core/src/main/java/accord/primitives/AbstractRanges.java b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
index a377221..44e1220 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractRanges.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
@@ -28,9 +28,9 @@ import javax.annotation.Nonnull;
 import java.util.*;
 import java.util.function.BiFunction;
 import java.util.function.Function;
-import java.util.function.Predicate;
 
 import static accord.utils.ArrayBuffers.cachedRanges;
+import static accord.utils.SortedArrays.Search.CEIL;
 import static accord.utils.SortedArrays.Search.FAST;
 import static accord.utils.SortedArrays.swapHighLow32b;
 
@@ -113,28 +113,19 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
         return size() == 0;
     }
 
-    public final boolean intersects(AbstractKeys<?, ?> keys)
+    public final boolean intersects(Routables<?, ?> keysOrRanges)
     {
-        return findNextIntersection(0, keys, 0) >= 0;
+        switch (keysOrRanges.kindOfContents())
+        {
+            default: throw new AssertionError();
+            case Key: return intersects((AbstractKeys<?, ?>) keysOrRanges);
+            case Range: return intersects((AbstractRanges<?>) keysOrRanges);
+        }
     }
 
-    public final <K extends RoutableKey> boolean intersects(AbstractKeys<K, ?> keys, Predicate<? super K> matches)
+    public final boolean intersects(AbstractKeys<?, ?> keys)
     {
-        int ri = 0, ki = 0;
-        while (true)
-        {
-            long rki = findNextIntersection(ri, keys, ki);
-            if (rki < 0)
-                return false;
-
-            ri = (int) (rki);
-            ki = (int) (rki >>> 32);
-
-            if (matches.test(keys.get(ki)))
-                return true;
-
-            ki++;
-        }
+        return findNextIntersection(0, keys, 0) >= 0;
     }
 
     public boolean intersects(AbstractRanges<?> that)
diff --git a/accord-core/src/main/java/accord/primitives/Deps.java b/accord-core/src/main/java/accord/primitives/Deps.java
index 6f9cc2c..ff0f7a0 100644
--- a/accord-core/src/main/java/accord/primitives/Deps.java
+++ b/accord-core/src/main/java/accord/primitives/Deps.java
@@ -1070,10 +1070,9 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
     /**
      * For each {@link TxnId} that references a key within the {@link Ranges}; the {@link TxnId} will be seen exactly once.
      * @param ranges to match on
-     * @param include function to say if a key should be used or not
      * @param forEach function to call on each unique {@link TxnId}
      */
-    public void forEachOn(Ranges ranges, Predicate<? super Key> include, Consumer<TxnId> forEach)
+    public void forEachOn(Ranges ranges, Consumer<TxnId> forEach)
     {
         // Find all keys within the ranges, but record existence within an int64 bitset.  Since the bitset is limited
         // to 64, this search must be called multiple times searching for different TxnIds in txnIds; this also has
@@ -1083,9 +1082,6 @@ public class Deps implements Iterable<Map.Entry<Key, TxnId>>
         for (int offset = 0 ; offset < txnIds.length ; offset += 64)
         {
             long bitset = Routables.foldl(keys, ranges, (key, off, value, keyIndex) -> {
-                if (!include.test(key))
-                    return value;
-
                 int index = startOffset(keyIndex);
                 int end = endOffset(keyIndex);
                 if (off > 0)
diff --git a/accord-core/src/main/java/accord/primitives/FullKeyRoute.java b/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
index c6abd45..cd9f3a9 100644
--- a/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
+++ b/accord-core/src/main/java/accord/primitives/FullKeyRoute.java
@@ -46,9 +46,9 @@ public class FullKeyRoute extends KeyRoute implements FullRoute<RoutingKey>
     }
 
     @Override
-    public PartialKeyRoute slice(Ranges ranges)
+    public PartialKeyRoute slice(Ranges newRanges)
     {
-        return new PartialKeyRoute(ranges, homeKey, slice(ranges, RoutingKey[]::new));
+        return new PartialKeyRoute(newRanges, homeKey, slice(newRanges, RoutingKey[]::new));
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/primitives/Keys.java b/accord-core/src/main/java/accord/primitives/Keys.java
index 0e075ba..2d9c7b8 100644
--- a/accord-core/src/main/java/accord/primitives/Keys.java
+++ b/accord-core/src/main/java/accord/primitives/Keys.java
@@ -74,7 +74,7 @@ public class Keys extends AbstractKeys<Key, Keys> implements Seekables<Key, Keys
     @Override
     public Keys slice(Ranges ranges)
     {
-        return wrap(SortedArrays.sliceWithMultipleMatches(keys, ranges.ranges, Key[]::new, (k, r) -> -r.compareTo(k), Range::compareTo));
+        return wrap(slice(ranges, Key[]::new));
     }
 
     public Keys with(Key key)
diff --git a/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java b/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
index 21c1ec0..c964aef 100644
--- a/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
+++ b/accord-core/src/main/java/accord/primitives/PartialKeyRoute.java
@@ -26,13 +26,13 @@ public class PartialKeyRoute extends KeyRoute implements PartialRoute<RoutingKey
         this.covering = covering;
     }
 
-    public PartialKeyRoute sliceStrict(Ranges newRange)
+    public PartialKeyRoute sliceStrict(Ranges newRanges)
     {
-        if (!covering.containsAll(newRange))
+        if (!covering.containsAll(newRanges))
             throw new IllegalArgumentException("Not covered");
 
-        RoutingKey[] keys = slice(newRange, RoutingKey[]::new);
-        return new PartialKeyRoute(newRange, homeKey, keys);
+        RoutingKey[] keys = slice(newRanges, RoutingKey[]::new);
+        return new PartialKeyRoute(newRanges, homeKey, keys);
     }
 
     @Override
@@ -70,9 +70,9 @@ public class PartialKeyRoute extends KeyRoute implements PartialRoute<RoutingKey
         return new RoutingKeys(toRoutingKeysArray(withKey));
     }
 
-    public PartialKeyRoute slice(Ranges newRange)
+    public PartialKeyRoute slice(Ranges newRanges)
     {
-        if (newRange.containsAll(covering))
+        if (newRanges.containsAll(covering))
             return this;
 
         RoutingKey[] keys = slice(covering, RoutingKey[]::new);
diff --git a/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java b/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
index c28239a..1cad856 100644
--- a/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
+++ b/accord-core/src/main/java/accord/primitives/PartialRangeRoute.java
@@ -65,12 +65,12 @@ public class PartialRangeRoute extends RangeRoute implements PartialRoute<Range>
         throw new UnsupportedOperationException();
     }
 
-    public PartialRangeRoute slice(Ranges newRange)
+    public PartialRangeRoute slice(Ranges newRanges)
     {
-        if (newRange.containsAll(covering))
+        if (newRanges.containsAll(covering))
             return this;
 
-        return slice(newRange, this, homeKey, PartialRangeRoute::new);
+        return slice(newRanges, this, homeKey, PartialRangeRoute::new);
     }
 
     public Unseekables<Range, ?> with(RoutingKey withKey)
diff --git a/accord-core/src/main/java/accord/primitives/Range.java b/accord-core/src/main/java/accord/primitives/Range.java
index c69894e..1a52d32 100644
--- a/accord-core/src/main/java/accord/primitives/Range.java
+++ b/accord-core/src/main/java/accord/primitives/Range.java
@@ -212,7 +212,7 @@ public abstract class Range implements Comparable<RoutableKey>, Unseekable, Seek
      */
     public abstract int compareTo(RoutableKey key);
 
-    public boolean containsKey(RoutableKey key)
+    public boolean contains(RoutableKey key)
     {
         return compareTo(key) == 0;
     }
@@ -232,12 +232,12 @@ public abstract class Range implements Comparable<RoutableKey>, Unseekable, Seek
         return 0;
     }
 
-    public boolean fullyContains(Range that)
+    public boolean contains(Range that)
     {
         return that.start.compareTo(this.start) >= 0 && that.end.compareTo(this.end) <= 0;
     }
 
-    public boolean intersects(Keys keys)
+    public boolean intersects(AbstractKeys<?, ?> keys)
     {
         return SortedArrays.binarySearch(keys.keys, 0, keys.size(), this, Range::compareTo, FAST) >= 0;
     }
@@ -287,7 +287,7 @@ public abstract class Range implements Comparable<RoutableKey>, Unseekable, Seek
     public static Range slice(Range bound, Range toSlice)
     {
         Invariants.checkArgument(bound.compareIntersecting(toSlice) == 0);
-        if (bound.fullyContains(toSlice))
+        if (bound.contains(toSlice))
             return toSlice;
 
         return toSlice.subRange(
diff --git a/accord-core/src/main/java/accord/primitives/RoutableKey.java b/accord-core/src/main/java/accord/primitives/RoutableKey.java
index 0e005ff..b6ba11b 100644
--- a/accord-core/src/main/java/accord/primitives/RoutableKey.java
+++ b/accord-core/src/main/java/accord/primitives/RoutableKey.java
@@ -32,9 +32,6 @@ public interface RoutableKey extends Routable, Comparable<RoutableKey>
             return compareTo;
         }
 
-        @Override
-        public int routingHash() { throw new UnsupportedOperationException(); }
-
         @Override
         public RoutingKey toUnseekable() { throw new UnsupportedOperationException(); }
 
@@ -49,13 +46,6 @@ public interface RoutableKey extends Routable, Comparable<RoutableKey>
      */
     int compareTo(@Nonnull RoutableKey that);
 
-    /**
-     * Returns a hash code of a key to support accord internal sharding. Hash values for equal keys must be equal.
-     *
-     * TODO (now): can we remove this if we remove hashIntersects et al?
-     */
-    int routingHash();
-
     default Kind kind() { return Kind.Key; }
 
     RoutingKey toUnseekable();
diff --git a/accord-core/src/main/java/accord/primitives/RoutingKeys.java b/accord-core/src/main/java/accord/primitives/RoutingKeys.java
index 523cce5..fd30ecf 100644
--- a/accord-core/src/main/java/accord/primitives/RoutingKeys.java
+++ b/accord-core/src/main/java/accord/primitives/RoutingKeys.java
@@ -47,7 +47,7 @@ public class RoutingKeys extends AbstractRoutableKeys<AbstractRoutableKeys<?>> i
 
     public RoutingKeys slice(Ranges ranges)
     {
-        return wrap(SortedArrays.sliceWithMultipleMatches(keys, ranges.ranges, RoutingKey[]::new, (k, r) -> -r.compareTo(k), Range::compareTo));
+        return wrap(slice(ranges, RoutingKey[]::new));
     }
 
     private RoutingKeys wrap(RoutingKey[] wrap, AbstractKeys<RoutingKey, ?> that)
diff --git a/accord-core/src/main/java/accord/primitives/Txn.java b/accord-core/src/main/java/accord/primitives/Txn.java
index 907bdaf..8703c04 100644
--- a/accord-core/src/main/java/accord/primitives/Txn.java
+++ b/accord-core/src/main/java/accord/primitives/Txn.java
@@ -176,9 +176,6 @@ public interface Txn
     {
         Ranges ranges = safeStore.ranges().at(command.executeAt().epoch);
         List<Future<Data>> futures = read().keys().foldl(ranges, (key, accumulate, index) -> {
-            if (!safeStore.commandStore().hashIntersects(key))
-                return accumulate;
-
             Future<Data> result = read().read(key, kind(), safeStore, command.executeAt(), safeStore.dataStore());
             accumulate.add(result);
             return accumulate;
diff --git a/accord-core/src/main/java/accord/primitives/Writes.java b/accord-core/src/main/java/accord/primitives/Writes.java
index b41d2bb..f8d90b5 100644
--- a/accord-core/src/main/java/accord/primitives/Writes.java
+++ b/accord-core/src/main/java/accord/primitives/Writes.java
@@ -72,8 +72,7 @@ public class Writes
             return SUCCESS;
 
         List<Future<Void>> futures = keys.foldl(ranges, (key, accumulate, index) -> {
-            if (safeStore.commandStore().hashIntersects(key))
-                accumulate.add(write.apply(key, safeStore, executeAt, safeStore.dataStore()));
+            accumulate.add(write.apply(key, safeStore, executeAt, safeStore.dataStore()));
             return accumulate;
         }, new ArrayList<>());
         return ReducingFuture.reduce(futures, (l, r) -> null);
diff --git a/accord-core/src/main/java/accord/topology/Shard.java b/accord-core/src/main/java/accord/topology/Shard.java
index 0e67f77..c749d40 100644
--- a/accord-core/src/main/java/accord/topology/Shard.java
+++ b/accord-core/src/main/java/accord/topology/Shard.java
@@ -97,7 +97,7 @@ public class Shard
 
     public boolean contains(Key key)
     {
-        return range.containsKey(key);
+        return range.contains(key);
     }
 
     public String toString(boolean extendedInfo)
diff --git a/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java b/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
index 0270e28..767d262 100644
--- a/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
+++ b/accord-core/src/main/java/accord/utils/IntrusiveLinkedListNode.java
@@ -40,5 +40,6 @@ public abstract class IntrusiveLinkedListNode
             next = null;
             prev = null;
         }
+        Invariants.paranoid(prev == null);
     }
 }
diff --git a/accord-core/src/main/java/accord/utils/Invariants.java b/accord-core/src/main/java/accord/utils/Invariants.java
index 0dea428..4242c5b 100644
--- a/accord-core/src/main/java/accord/utils/Invariants.java
+++ b/accord-core/src/main/java/accord/utils/Invariants.java
@@ -6,6 +6,8 @@ import java.util.function.Predicate;
 
 public class Invariants
 {
+    private static final boolean PARANOID = true;
+
     public static <T1, T2 extends T1> T2 checkType(T1 cast)
     {
         return (T2)cast;
@@ -25,6 +27,12 @@ public class Invariants
         return (T2)cast;
     }
 
+    public static void paranoid(boolean condition)
+    {
+        if (PARANOID && !condition)
+            throw new IllegalStateException();
+    }
+
     public static void checkState(boolean condition)
     {
         if (!condition)
diff --git a/accord-core/src/test/java/accord/impl/IntHashKey.java b/accord-core/src/test/java/accord/impl/IntHashKey.java
index 1070370..75407f7 100644
--- a/accord-core/src/test/java/accord/impl/IntHashKey.java
+++ b/accord-core/src/test/java/accord/impl/IntHashKey.java
@@ -24,38 +24,91 @@ import java.util.Objects;
 import java.util.zip.CRC32;
 
 import accord.api.RoutingKey;
+import accord.local.ShardDistributor;
 import accord.primitives.RoutableKey;
 import accord.primitives.Ranges;
 import accord.primitives.Keys;
+import accord.utils.Invariants;
 
 import static accord.utils.Utils.toArray;
 import javax.annotation.Nonnull;
 
 public abstract class IntHashKey implements RoutableKey
 {
-    public static final class Key extends IntHashKey implements accord.api.Key
+    public static class Splitter implements ShardDistributor.EvenSplit.Splitter<Long>
     {
-        private Key(int key)
+        @Override
+        public Long sizeOf(accord.primitives.Range range)
         {
-            super(key);
+            return ((IntHashKey)range.end()).hash - (long)((IntHashKey)range.start()).hash;
+        }
+
+        @Override
+        public accord.primitives.Range subRange(accord.primitives.Range range, Long start, Long end)
+        {
+            Invariants.checkArgument(((IntHashKey)range.start()).hash + end.intValue() <= ((IntHashKey)range.end()).hash);
+            return range.subRange(
+                    new Hash(((IntHashKey)range.start()).hash + start.intValue()),
+                    new Hash(((IntHashKey)range.start()).hash + end.intValue())
+            );
+        }
+
+        @Override
+        public Long zero()
+        {
+            return 0L;
+        }
+
+        @Override
+        public Long add(Long a, Long b)
+        {
+            return a + b;
         }
 
-        private Key(int key, int hash)
+        @Override
+        public Long subtract(Long a, Long b)
         {
-            super(key, hash);
+            return a - b;
+        }
+
+        @Override
+        public Long divide(Long a, int i)
+        {
+            return a / i;
+        }
+
+        @Override
+        public Long multiply(Long a, int i)
+        {
+            return a * i;
+        }
+
+        @Override
+        public int min(Long v, int i)
+        {
+            return (int)Math.min(v, i);
+        }
+
+        @Override
+        public int compare(Long a, Long b)
+        {
+            return a.compareTo(b);
         }
     }
 
-    public static final class Hash extends IntHashKey implements RoutingKey
+    public static final class Key extends IntHashKey implements accord.api.Key
     {
-        private Hash(int key)
+        private Key(int key)
         {
             super(key);
         }
+    }
 
-        private Hash(int key, int hash)
+    public static final class Hash extends IntHashKey implements RoutingKey
+    {
+        public Hash(int hash)
         {
-            super(key, hash);
+            super(Integer.MIN_VALUE, hash);
         }
     }
 
@@ -87,8 +140,8 @@ public abstract class IntHashKey implements RoutableKey
             {
                 int subStart = i > 0 ? last : startHash;
                 int subEnd = i < count - 1 ? subStart + interval : endHash;
-                ranges[i] = new Range(new Hash(Integer.MIN_VALUE, subStart),
-                                      new Hash(Integer.MIN_VALUE, subEnd));
+                ranges[i] = new Range(new Hash(subStart),
+                                      new Hash(subEnd));
                 last = subEnd;
             }
             return Ranges.ofSortedAndDeoverlapped(ranges);
@@ -118,7 +171,7 @@ public abstract class IntHashKey implements RoutableKey
 
     public static Hash forHash(int hash)
     {
-        return new Hash(Integer.MIN_VALUE, hash);
+        return new Hash(hash);
     }
 
     public static Keys keys(int k0, int... kn)
@@ -136,14 +189,14 @@ public abstract class IntHashKey implements RoutableKey
         List<accord.primitives.Range> result = new ArrayList<>();
         long delta = (Integer.MAX_VALUE - (long)Integer.MIN_VALUE) / count;
         long start = Integer.MIN_VALUE;
-        Hash prev = new Hash(Integer.MIN_VALUE, (int)start);
+        Hash prev = new Hash((int)start);
         for (int i = 1 ; i < count ; ++i)
         {
-            Hash next = new Hash(Integer.MIN_VALUE, (int)Math.min(Integer.MAX_VALUE, start + i * delta));
+            Hash next = new Hash((int)Math.min(Integer.MAX_VALUE, start + i * delta));
             result.add(new Range(prev, next));
             prev = next;
         }
-        result.add(new Range(prev, new Hash(Integer.MIN_VALUE, Integer.MAX_VALUE)));
+        result.add(new Range(prev, new Hash(Integer.MAX_VALUE)));
         return toArray(result, accord.primitives.Range[]::new);
     }
 
@@ -184,17 +237,11 @@ public abstract class IntHashKey implements RoutableKey
         return (int)crc32c.getValue();
     }
 
-    @Override
-    public int routingHash()
-    {
-        return hash;
-    }
-
     @Override
     public RoutingKey toUnseekable()
     {
         if (key == Integer.MIN_VALUE)
-            return this instanceof Hash ? (Hash)this : new Hash(Integer.MIN_VALUE, hash);
+            return this instanceof Hash ? (Hash)this : new Hash(hash);
 
         return forHash(hash);
     }
diff --git a/accord-core/src/test/java/accord/impl/IntKey.java b/accord-core/src/test/java/accord/impl/IntKey.java
index 6998e9c..379b110 100644
--- a/accord-core/src/test/java/accord/impl/IntKey.java
+++ b/accord-core/src/test/java/accord/impl/IntKey.java
@@ -23,9 +23,11 @@ import java.util.List;
 import java.util.Objects;
 
 import accord.api.RoutingKey;
+import accord.local.ShardDistributor;
 import accord.primitives.RoutableKey;
 import accord.primitives.Keys;
 import accord.primitives.RoutingKeys;
+import accord.utils.Invariants;
 
 import javax.annotation.Nonnull;
 
@@ -33,6 +35,67 @@ import static accord.utils.Utils.toArray;
 
 public class IntKey implements RoutableKey
 {
+    public static class Splitter implements ShardDistributor.EvenSplit.Splitter<Long>
+    {
+        @Override
+        public Long sizeOf(accord.primitives.Range range)
+        {
+            return ((IntKey)range.end()).key - (long)((IntKey)range.start()).key;
+        }
+
+        @Override
+        public accord.primitives.Range subRange(accord.primitives.Range range, Long start, Long end)
+        {
+            Invariants.checkArgument(((IntKey)range.start()).key + end.intValue() <= ((IntKey)range.end()).key);
+            return range.subRange(
+                    routing(((IntKey)range.start()).key + start.intValue()),
+                    routing(((IntKey)range.start()).key + end.intValue())
+                    );
+        }
+
+        @Override
+        public Long zero()
+        {
+            return 0L;
+        }
+
+        @Override
+        public Long add(Long a, Long b)
+        {
+            return a + b;
+        }
+
+        @Override
+        public Long subtract(Long a, Long b)
+        {
+            return a - b;
+        }
+
+        @Override
+        public Long divide(Long a, int i)
+        {
+            return a / i;
+        }
+
+        @Override
+        public Long multiply(Long a, int i)
+        {
+            return a * i;
+        }
+
+        @Override
+        public int min(Long v, int i)
+        {
+            return (int)Math.min(v, i);
+        }
+
+        @Override
+        public int compare(Long a, Long b)
+        {
+            return a.compareTo(b);
+        }
+    }
+
     public static class Raw extends IntKey implements accord.api.Key
     {
         public Raw(int key)
@@ -157,12 +220,6 @@ public class IntKey implements RoutableKey
         return Objects.hash(key);
     }
 
-    @Override
-    public int routingHash()
-    {
-        return hashCode();
-    }
-
     @Override
     public RoutingKey toUnseekable()
     {
diff --git a/accord-core/src/test/java/accord/impl/basic/Cluster.java b/accord-core/src/test/java/accord/impl/basic/Cluster.java
index 9e2988e..f6afbdf 100644
--- a/accord-core/src/test/java/accord/impl/basic/Cluster.java
+++ b/accord-core/src/test/java/accord/impl/basic/Cluster.java
@@ -38,15 +38,13 @@ import java.util.function.Supplier;
 import accord.api.MessageSink;
 import accord.burn.BurnTestConfigurationService;
 import accord.burn.TopologyUpdates;
-import accord.impl.SimpleProgressLog;
-import accord.impl.InMemoryCommandStores;
-import accord.impl.SizeOfIntersectionSorter;
+import accord.impl.*;
 import accord.local.Node;
 import accord.local.Node.Id;
 import accord.api.Scheduler;
-import accord.impl.TopologyFactory;
 import accord.impl.list.ListAgent;
 import accord.impl.list.ListStore;
+import accord.local.ShardDistributor;
 import accord.messages.Callback;
 import accord.messages.Reply;
 import accord.messages.Request;
@@ -224,8 +222,9 @@ public class Cluster implements Scheduler
             {
                 MessageSink messageSink = sinks.create(node, randomSupplier.get());
                 BurnTestConfigurationService configService = new BurnTestConfigurationService(node, messageSink, randomSupplier, topology, lookup::get, topologyUpdates);
-                lookup.put(node, new Node(node, messageSink, configService,
-                                          nowSupplier.get(), () -> new ListStore(node), new ListAgent(30L, onFailure),
+                lookup.put(node, new Node(node, messageSink, configService, nowSupplier.get(),
+                                          () -> new ListStore(node), new ShardDistributor.EvenSplit<>(8, ignore -> new IntHashKey.Splitter()),
+                                          new ListAgent(30L, onFailure),
                                           randomSupplier.get(), sinks, SizeOfIntersectionSorter.SUPPLIER,
                                           SimpleProgressLog::new, InMemoryCommandStores.Synchronized::new));
             }
diff --git a/accord-core/src/test/java/accord/impl/mock/MockCluster.java b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
index 7111335..9009898 100644
--- a/accord-core/src/test/java/accord/impl/mock/MockCluster.java
+++ b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
@@ -24,6 +24,7 @@ import accord.coordinate.Timeout;
 import accord.impl.*;
 import accord.local.Node;
 import accord.local.Node.Id;
+import accord.local.ShardDistributor;
 import accord.primitives.Ranges;
 import accord.utils.EpochFunction;
 import accord.utils.ThreadPoolScheduler;
@@ -104,6 +105,7 @@ public class MockCluster implements Network, AutoCloseable, Iterable<Node>
                         configurationService,
                         nowSupplier,
                         () -> store,
+                        new ShardDistributor.EvenSplit(8, ignore -> new IntKey.Splitter()),
                         new TestAgent(),
                         new Random(random.nextLong()),
                         new ThreadPoolScheduler(),
diff --git a/accord-core/src/test/java/accord/local/CommandTest.java b/accord-core/src/test/java/accord/local/CommandTest.java
index 0a5599a..1b57025 100644
--- a/accord-core/src/test/java/accord/local/CommandTest.java
+++ b/accord-core/src/test/java/accord/local/CommandTest.java
@@ -138,7 +138,7 @@ public class CommandTest
     private static Node createNode(Id id, CommandStoreSupport storeSupport)
     {
         return new Node(id, null, new MockConfigurationService(null, (epoch, service) -> { }, storeSupport.local.get()),
-                        new MockCluster.Clock(100), () -> storeSupport.data, new TestAgent(), new Random(), null,
+                        new MockCluster.Clock(100), () -> storeSupport.data, new ShardDistributor.EvenSplit(8, ignore -> new IntKey.Splitter()), new TestAgent(), new Random(), null,
                         SizeOfIntersectionSorter.SUPPLIER, ignore -> ignore2 -> new NoOpProgressLog(), InMemoryCommandStores.Synchronized::new);
     }
 
diff --git a/accord-core/src/test/java/accord/messages/PreAcceptTest.java b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
index cfa371a..a8ae211 100644
--- a/accord-core/src/test/java/accord/messages/PreAcceptTest.java
+++ b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
@@ -67,6 +67,7 @@ public class PreAcceptTest
                         new MockConfigurationService(messageSink, EpochFunction.noop(), TOPOLOGY),
                         clock,
                         () -> store,
+                        new ShardDistributor.EvenSplit(8, ignore -> new IntKey.Splitter()),
                         new TestAgent(),
                         new Random(),
                         scheduler,
diff --git a/accord-core/src/test/java/accord/topology/TopologyTest.java b/accord-core/src/test/java/accord/topology/TopologyTest.java
index c7a61a2..6631c15 100644
--- a/accord-core/src/test/java/accord/topology/TopologyTest.java
+++ b/accord-core/src/test/java/accord/topology/TopologyTest.java
@@ -40,13 +40,13 @@ public class TopologyTest
         Key expectedKey = key(key);
         Shard shard = topology.forKey(routing(key));
         Range expectedRange = range(start, end);
-        Assertions.assertTrue(expectedRange.containsKey(expectedKey));
-        Assertions.assertTrue(shard.range.containsKey(expectedKey));
+        Assertions.assertTrue(expectedRange.contains(expectedKey));
+        Assertions.assertTrue(shard.range.contains(expectedKey));
         Assertions.assertEquals(expectedRange, shard.range);
 
         Topology subTopology = topology.forSelection(Keys.of(expectedKey).toUnseekables());
         shard = Iterables.getOnlyElement(subTopology.shards());
-        Assertions.assertTrue(shard.range.containsKey(expectedKey));
+        Assertions.assertTrue(shard.range.contains(expectedKey));
         Assertions.assertEquals(expectedRange, shard.range);
     }
 
diff --git a/accord-core/src/test/java/accord/txn/DepsTest.java b/accord-core/src/test/java/accord/txn/DepsTest.java
index ac8e3d6..00fbb64 100644
--- a/accord-core/src/test/java/accord/txn/DepsTest.java
+++ b/accord-core/src/test/java/accord/txn/DepsTest.java
@@ -181,7 +181,7 @@ public class DepsTest
                 throw new AssertionError(start + " == " + end);
 
             TreeSet<TxnId> seen = new TreeSet<>();
-            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), false, true)), ignore -> true, txnId -> {
+            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), false, true)), txnId -> {
                 if (!seen.add(txnId))
                     throw new AssertionError("Seen " + txnId + " multiple times");
             });
@@ -205,7 +205,7 @@ public class DepsTest
             Key end = keys.get(keys.size() - 1);
 
             TreeSet<TxnId> seen = new TreeSet<>();
-            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), true, false)), ignore -> true, txnId -> {
+            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), true, false)), txnId -> {
                 if (!seen.add(txnId))
                     throw new AssertionError("Seen " + txnId + " multiple times");
             });
@@ -229,7 +229,7 @@ public class DepsTest
             Key end = keys.get(0);
 
             TreeSet<TxnId> seen = new TreeSet<>();
-            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), true, false)), ignore -> true, txnId -> {
+            deps.test.forEachOn(Ranges.of(Range.range(start.toUnseekable(), end.toUnseekable(), true, false)), txnId -> {
                 if (!seen.add(txnId))
                     throw new AssertionError("Seen " + txnId + " multiple times");
             });
@@ -542,7 +542,7 @@ public class DepsTest
                         if (ranges.contains(key))
                             canonical.addAll(deps.canonical.get(key));
                     }
-                    deps.test.forEachOn(ranges, ignore -> true, txnId -> test.add(txnId));
+                    deps.test.forEachOn(ranges, test::add);
                     test.sort(Timestamp::compareTo);
                     Assertions.assertEquals(new ArrayList<>(canonical), test);
                 }
diff --git a/accord-core/src/test/java/accord/utils/RangeTest.java b/accord-core/src/test/java/accord/utils/RangeTest.java
index 6f083f3..2ad585b 100644
--- a/accord-core/src/test/java/accord/utils/RangeTest.java
+++ b/accord-core/src/test/java/accord/utils/RangeTest.java
@@ -97,22 +97,22 @@ public class RangeTest
     void containsTest()
     {
         Range endInclRange = rangeEndIncl(10, 20);
-        Assertions.assertFalse(endInclRange.containsKey(k(10)));
+        Assertions.assertFalse(endInclRange.contains(k(10)));
         Assertions.assertFalse(endInclRange.startInclusive());
-        Assertions.assertTrue(endInclRange.containsKey(k(20)));
+        Assertions.assertTrue(endInclRange.contains(k(20)));
         Assertions.assertTrue(endInclRange.endInclusive());
 
         Range startInclRange = rangeStartIncl(10, 20);
-        Assertions.assertTrue(startInclRange.containsKey(k(10)));
+        Assertions.assertTrue(startInclRange.contains(k(10)));
         Assertions.assertTrue(startInclRange.startInclusive());
-        Assertions.assertFalse(startInclRange.containsKey(k(20)));
+        Assertions.assertFalse(startInclRange.contains(k(20)));
         Assertions.assertFalse(startInclRange.endInclusive());
     }
 
     private static void assertHigherKeyIndex(int expectedIdx, Range range, Keys keys)
     {
         if (expectedIdx > 0 && expectedIdx < keys.size())
-            Assertions.assertTrue(range.containsKey(keys.get(expectedIdx - 1)));
+            Assertions.assertTrue(range.contains(keys.get(expectedIdx - 1)));
         int actualIdx = range.nextHigherKeyIndex(keys, 0);
         Assertions.assertEquals(expectedIdx, actualIdx);
     }
@@ -143,12 +143,12 @@ public class RangeTest
     {
         if (expectedIdx >= 0 && expectedIdx < keys.size())
         {
-            Assertions.assertTrue(range.containsKey(keys.get(expectedIdx)));
+            Assertions.assertTrue(range.contains(keys.get(expectedIdx)));
         }
         else
         {
-            Assertions.assertFalse(range.containsKey(keys.get(lowerBound)));
-            Assertions.assertFalse(range.containsKey(keys.get(keys.size() - 1)));
+            Assertions.assertFalse(range.contains(keys.get(lowerBound)));
+            Assertions.assertFalse(range.contains(keys.get(keys.size() - 1)));
         }
 
         int actualIdx = range.nextCeilKeyIndex(keys, lowerBound);
@@ -190,17 +190,17 @@ public class RangeTest
     @Test
     void fullyContainsTest()
     {
-        Assertions.assertTrue(r(100, 200).fullyContains(r(100, 200)));
-        Assertions.assertTrue(r(100, 200).fullyContains(r(150, 200)));
-        Assertions.assertTrue(r(100, 200).fullyContains(r(100, 150)));
-        Assertions.assertTrue(r(100, 200).fullyContains(r(125, 175)));
-
-        Assertions.assertFalse(r(100, 200).fullyContains(r(50, 60)));
-        Assertions.assertFalse(r(100, 200).fullyContains(r(100, 250)));
-        Assertions.assertFalse(r(100, 200).fullyContains(r(150, 250)));
-        Assertions.assertFalse(r(100, 200).fullyContains(r(50, 200)));
-        Assertions.assertFalse(r(100, 200).fullyContains(r(50, 150)));
-        Assertions.assertFalse(r(100, 200).fullyContains(r(250, 260)));
+        Assertions.assertTrue(r(100, 200).contains(r(100, 200)));
+        Assertions.assertTrue(r(100, 200).contains(r(150, 200)));
+        Assertions.assertTrue(r(100, 200).contains(r(100, 150)));
+        Assertions.assertTrue(r(100, 200).contains(r(125, 175)));
+
+        Assertions.assertFalse(r(100, 200).contains(r(50, 60)));
+        Assertions.assertFalse(r(100, 200).contains(r(100, 250)));
+        Assertions.assertFalse(r(100, 200).contains(r(150, 250)));
+        Assertions.assertFalse(r(100, 200).contains(r(50, 200)));
+        Assertions.assertFalse(r(100, 200).contains(r(50, 150)));
+        Assertions.assertFalse(r(100, 200).contains(r(250, 260)));
     }
 
     @Test
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
index 64b0ec0..bba292e 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
@@ -39,6 +39,7 @@ import accord.impl.InMemoryCommandStores;
 import accord.local.Node;
 import accord.local.Node.Id;
 import accord.api.MessageSink;
+import accord.local.ShardDistributor;
 import accord.messages.Callback;
 import accord.messages.Reply;
 import accord.messages.ReplyContext;
@@ -133,6 +134,9 @@ public class Cluster implements Scheduler
 
     private void add(Packet packet)
     {
+        if (packet == null)
+            throw new IllegalArgumentException();
+
         err.println(clock++ + " SEND " + packet);
         err.flush();
         if (lookup.apply(packet.dest) == null) responseSink.accept(packet);
@@ -296,7 +300,8 @@ public class Cluster implements Scheduler
             {
                 MessageSink messageSink = sinks.create(node, randomSupplier.get());
                 lookup.put(node, new Node(node, messageSink, new SimpleConfigService(topology),
-                                          nowSupplier.get(), MaelstromStore::new, MaelstromAgent.INSTANCE,
+                                          nowSupplier.get(), MaelstromStore::new, new ShardDistributor.EvenSplit(8, ignore -> new MaelstromKey.Splitter()),
+                                          MaelstromAgent.INSTANCE,
                                           randomSupplier.get(), sinks, SizeOfIntersectionSorter.SUPPLIER,
                                           SimpleProgressLog::new, InMemoryCommandStores.SingleThread::new));
             }
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
index ee24370..f311261 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
@@ -124,6 +124,11 @@ public class Json
 
     private static <T> T readTimestamp(JsonReader in, TimestampFactory<T> factory) throws IOException
     {
+        if (in.peek() == JsonToken.NULL)
+        {
+            in.nextNull();
+            return null;
+        }
         in.beginArray();
         long epoch = in.nextLong();
         long real = in.nextLong();
@@ -135,6 +140,11 @@ public class Json
 
     private static void writeTimestamp(JsonWriter out, Timestamp timestamp) throws IOException
     {
+        if (timestamp == null)
+        {
+            out.nullValue();
+            return;
+        }
         out.beginArray();
         out.value(timestamp.epoch);
         out.value(timestamp.real);
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
index 319bc5c..057b335 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
@@ -22,7 +22,9 @@ import java.io.IOException;
 
 import accord.api.RoutingKey;
 
+import accord.local.ShardDistributor;
 import accord.primitives.RoutableKey;
+import accord.utils.Invariants;
 import com.google.gson.TypeAdapter;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
@@ -31,6 +33,77 @@ import javax.annotation.Nonnull;
 
 public class MaelstromKey implements RoutableKey
 {
+    public static class Splitter implements ShardDistributor.EvenSplit.Splitter<Long>
+    {
+        private static long hash(RoutingKey routingKey)
+        {
+            Datum.Hash hash = ((Datum.Hash)((MaelstromKey)routingKey).datum.value);
+            if (hash == null)
+                return Integer.MAX_VALUE;
+            return hash.hash;
+        }
+
+        @Override
+        public Long sizeOf(accord.primitives.Range range)
+        {
+            return hash(range.end()) - hash(range.start());
+        }
+
+        @Override
+        public accord.primitives.Range subRange(accord.primitives.Range range, Long start, Long end)
+        {
+            Invariants.checkState(end - start <= Integer.MAX_VALUE);
+            long startHash = hash(range.start());
+            Invariants.checkArgument(startHash + end <= hash(range.end()));
+            return range.subRange(
+                    new Routing(Datum.Kind.HASH, new Datum.Hash((int) (startHash + start))),
+                    new Routing(Datum.Kind.HASH, new Datum.Hash((int) (startHash + end)))
+            );
+        }
+
+        @Override
+        public Long zero()
+        {
+            return 0L;
+        }
+
+        @Override
+        public Long add(Long a, Long b)
+        {
+            return a + b;
+        }
+
+        @Override
+        public Long subtract(Long a, Long b)
+        {
+            return a - b;
+        }
+
+        @Override
+        public Long divide(Long a, int i)
+        {
+            return a / i;
+        }
+
+        @Override
+        public Long multiply(Long a, int i)
+        {
+            return a * i;
+        }
+
+        @Override
+        public int min(Long v, int i)
+        {
+            return (int)Math.min(v, i);
+        }
+
+        @Override
+        public int compare(Long a, Long b)
+        {
+            return a.compareTo(b);
+        }
+    }
+
     public static class Key extends MaelstromKey implements accord.api.Key
     {
         public Key(Datum.Kind kind, Object value)
@@ -129,12 +202,6 @@ public class MaelstromKey implements RoutableKey
         }
     };
 
-    @Override
-    public int routingHash()
-    {
-        return datum.hashCode();
-    }
-
     @Override
     public RoutingKey toUnseekable()
     {
@@ -142,4 +209,10 @@ public class MaelstromKey implements RoutableKey
             return (Routing)this;
         return new Routing(datum.kind, datum.value);
     }
+
+    @Override
+    public String toString()
+    {
+        return datum.toString();
+    }
 }
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Main.java b/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
index d861bf7..1df7137 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
@@ -38,6 +38,7 @@ import accord.impl.SizeOfIntersectionSorter;
 import accord.local.Node;
 import accord.local.Node.Id;
 import accord.api.Scheduler;
+import accord.local.ShardDistributor;
 import accord.messages.ReplyContext;
 import accord.topology.Topology;
 import accord.utils.ThreadPoolScheduler;
@@ -160,8 +161,9 @@ public class Main
             topology = topologyFactory.toTopology(init.cluster);
             sink = new StdoutSink(System::currentTimeMillis, scheduler, start, init.self, out, err);
             on = new Node(init.self, sink, new SimpleConfigService(topology), System::currentTimeMillis,
-                          MaelstromStore::new, MaelstromAgent.INSTANCE, new Random(), scheduler,
-                          SizeOfIntersectionSorter.SUPPLIER, SimpleProgressLog::new, InMemoryCommandStores.SingleThread::new);
+                          MaelstromStore::new, new ShardDistributor.EvenSplit(8, ignore -> new MaelstromKey.Splitter()),
+                          MaelstromAgent.INSTANCE, new Random(), scheduler, SizeOfIntersectionSorter.SUPPLIER,
+                          SimpleProgressLog::new, InMemoryCommandStores.SingleThread::new);
             err.println("Initialized node " + init.self);
             err.flush();
             sink.send(packet.src, new Body(Type.init_ok, Body.SENTINEL_MSG_ID, init.msg_id));


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org


[cassandra-accord] 03/03: Refactor Timestamp/TxnId (#22)

Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

benedict pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git

commit d9cb3b555dd2467f3e68e313d00aa808c617c8d4
Author: Benedict Elliott Smith <be...@apache.org>
AuthorDate: Wed Jan 11 18:05:22 2023 +0000

    Refactor Timestamp/TxnId (#22)
    
    - Combine real and logical into a single 64-bit HLC
    - Introduce 16 flag bits
    - Pack epoch (48-bits), HLC (64-bits) and flags (16-bits) into two longs in memory
---
 .../src/main/java/accord/coordinate/CheckOn.java   |   8 +-
 .../main/java/accord/coordinate/CheckShards.java   |   4 +-
 .../main/java/accord/coordinate/Coordinate.java    |  10 +-
 .../src/main/java/accord/coordinate/Execute.java   |  14 +-
 .../src/main/java/accord/coordinate/FetchData.java |   8 +-
 .../main/java/accord/coordinate/FindHomeKey.java   |   2 +-
 .../src/main/java/accord/coordinate/FindRoute.java |   2 +-
 .../java/accord/coordinate/InformHomeOfTxn.java    |   4 +-
 .../main/java/accord/coordinate/Invalidate.java    |   4 +-
 .../main/java/accord/coordinate/MaybeRecover.java  |   2 +-
 .../src/main/java/accord/coordinate/Persist.java   |  16 +-
 .../src/main/java/accord/coordinate/Propose.java   |   2 +-
 .../src/main/java/accord/coordinate/Recover.java   |  16 +-
 .../java/accord/coordinate/RecoverWithHomeKey.java |   2 +-
 .../java/accord/coordinate/RecoverWithRoute.java   |  12 +-
 .../src/main/java/accord/impl/InMemoryCommand.java |  14 --
 .../java/accord/impl/InMemoryCommandStore.java     |  15 +-
 .../java/accord/impl/InMemoryCommandsForKey.java   |   5 +-
 .../main/java/accord/impl/SimpleProgressLog.java   |  12 +-
 .../src/main/java/accord/local/Command.java        |  23 +--
 accord-core/src/main/java/accord/local/Node.java   |  39 +++--
 .../src/main/java/accord/messages/Accept.java      |   8 +-
 .../src/main/java/accord/messages/Apply.java       |   6 +-
 .../java/accord/messages/BeginInvalidation.java    |   5 +-
 .../main/java/accord/messages/BeginRecovery.java   |   6 +-
 .../src/main/java/accord/messages/Commit.java      |  20 +--
 .../src/main/java/accord/messages/GetDeps.java     |   4 +-
 .../main/java/accord/messages/InformDurable.java   |   4 +-
 .../java/accord/messages/InformHomeDurable.java    |   2 +-
 .../main/java/accord/messages/InformOfTxnId.java   |   7 +-
 .../src/main/java/accord/messages/PreAccept.java   |   2 +-
 .../src/main/java/accord/messages/ReadData.java    |   2 +-
 .../src/main/java/accord/messages/TxnRequest.java  |  12 +-
 .../main/java/accord/messages/WaitOnCommit.java    |   4 +-
 .../main/java/accord/primitives/AbstractKeys.java  |   4 +-
 .../java/accord/primitives/AbstractRanges.java     |   4 +-
 .../src/main/java/accord/primitives/Ballot.java    |  29 +++-
 .../src/main/java/accord/primitives/Range.java     |   2 +-
 .../src/main/java/accord/primitives/Routable.java  |  24 ++-
 .../main/java/accord/primitives/RoutableKey.java   |   2 +-
 .../src/main/java/accord/primitives/Routables.java |   2 +-
 .../src/main/java/accord/primitives/Timestamp.java | 182 ++++++++++++++++++---
 .../src/main/java/accord/primitives/Txn.java       |  24 ++-
 .../src/main/java/accord/primitives/TxnId.java     |  98 ++++++++++-
 .../src/main/java/accord/primitives/Writes.java    |   2 +-
 .../main/java/accord/topology/TopologyManager.java |   4 +-
 .../src/test/java/accord/burn/TopologyUpdates.java |   4 +-
 .../java/accord/coordinate/CoordinateTest.java     |  10 +-
 .../java/accord/coordinate/TopologyChangeTest.java |  21 +--
 .../src/test/java/accord/impl/TestAgent.java       |   2 +-
 .../src/test/java/accord/impl/list/ListAgent.java  |   2 +-
 .../test/java/accord/impl/list/ListRequest.java    |   3 +-
 .../test/java/accord/impl/mock/MockCluster.java    |   6 +-
 .../src/test/java/accord/local/CommandTest.java    |   6 +-
 .../src/test/java/accord/local/NodeTest.java       |  31 ++--
 .../test/java/accord/messages/PreAcceptTest.java   |  18 +-
 accord-core/src/test/java/accord/txn/DepsTest.java |  43 +++--
 .../src/main/java/accord/maelstrom/Json.java       |  20 +--
 .../main/java/accord/maelstrom/MaelstromAgent.java |   2 +-
 59 files changed, 550 insertions(+), 291 deletions(-)

diff --git a/accord-core/src/main/java/accord/coordinate/CheckOn.java b/accord-core/src/main/java/accord/coordinate/CheckOn.java
index d37a827..a7ec61f 100644
--- a/accord-core/src/main/java/accord/coordinate/CheckOn.java
+++ b/accord-core/src/main/java/accord/coordinate/CheckOn.java
@@ -136,7 +136,7 @@ public class CheckOn extends CheckShards
 
         public OnDone()
         {
-            Ranges localRanges = node.topology().localRangesForEpochs(txnId.epoch, untilLocalEpoch);
+            Ranges localRanges = node.topology().localRangesForEpochs(txnId.epoch(), untilLocalEpoch);
             PartialRoute<?> selfRoute = route().slice(localRanges);
             full = (CheckStatusOkFull) merged;
             sufficientFor = full.sufficientFor(selfRoute);
@@ -165,7 +165,7 @@ public class CheckOn extends CheckShards
                 txnIds = Iterables.concat(txnIds, partialDeps.txnIds());
 
             PreLoadContext loadContext = contextFor(txnIds, keys);
-            node.mapReduceConsumeLocal(loadContext, route, txnId.epoch, untilLocalEpoch, this);
+            node.mapReduceConsumeLocal(loadContext, route, txnId.epoch(), untilLocalEpoch, this);
         }
 
         @Override
@@ -187,7 +187,7 @@ public class CheckOn extends CheckShards
 
                 case Applied:
                 case PreApplied:
-                    if (untilLocalEpoch >= full.executeAt.epoch)
+                    if (untilLocalEpoch >= full.executeAt.epoch())
                     {
                         confirm(command.commit(safeStore, maxRoute, progressKey, partialTxn, full.executeAt, partialDeps));
                         confirm(command.apply(safeStore, untilLocalEpoch, maxRoute, full.executeAt, partialDeps, full.writes, full.result));
@@ -216,7 +216,7 @@ public class CheckOn extends CheckShards
             if (!merged.durability.isDurable() || homeKey == null)
                 return null;
 
-            if (!safeStore.ranges().at(txnId.epoch).contains(homeKey))
+            if (!safeStore.ranges().at(txnId.epoch()).contains(homeKey))
                 return null;
 
             Timestamp executeAt = merged.saveStatus.known.executeAt.isDecisionKnown() ? merged.executeAt : null;
diff --git a/accord-core/src/main/java/accord/coordinate/CheckShards.java b/accord-core/src/main/java/accord/coordinate/CheckShards.java
index 9ad05fd..5500d90 100644
--- a/accord-core/src/main/java/accord/coordinate/CheckShards.java
+++ b/accord-core/src/main/java/accord/coordinate/CheckShards.java
@@ -36,13 +36,13 @@ public abstract class CheckShards extends ReadCoordinator<CheckStatusReply>
 
     private static Topologies topologyFor(Node node, TxnId txnId, Unseekables<?, ?> contact, long epoch)
     {
-        return node.topology().preciseEpochs(contact, txnId.epoch, epoch);
+        return node.topology().preciseEpochs(contact, txnId.epoch(), epoch);
     }
 
     @Override
     protected void contact(Id id)
     {
-        node.send(id, new CheckStatus(txnId, contact.slice(topologies().computeRangesForNode(id)), txnId.epoch, untilRemoteEpoch, includeInfo), this);
+        node.send(id, new CheckStatus(txnId, contact.slice(topologies().computeRangesForNode(id)), txnId.epoch(), untilRemoteEpoch, includeInfo), this);
     }
 
     protected boolean isSufficient(Id from, CheckStatusOk ok) { return isSufficient(ok); }
diff --git a/accord-core/src/main/java/accord/coordinate/Coordinate.java b/accord-core/src/main/java/accord/coordinate/Coordinate.java
index ae65014..dec539c 100644
--- a/accord-core/src/main/java/accord/coordinate/Coordinate.java
+++ b/accord-core/src/main/java/accord/coordinate/Coordinate.java
@@ -155,7 +155,7 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
             //                                  but by sending accept we rule out hybrid fast-path
             // TODO (low priority, efficiency): if we receive an expired response, perhaps defer to permit at least one other
             //                                  node to respond before invalidating
-            if (node.agent().isExpired(txnId, executeAt.real))
+            if (node.agent().isExpired(txnId, executeAt.hlc()))
             {
                 proposeInvalidate(node, Ballot.ZERO, txnId, route.homeKey(), (success, fail) -> {
                     if (fail != null)
@@ -164,7 +164,7 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
                     }
                     else
                     {
-                        node.withEpoch(executeAt.epoch, () -> {
+                        node.withEpoch(executeAt.epoch(), () -> {
                             commitInvalidate(node, txnId, route, executeAt);
                             // TODO (required, API consistency): this should be Invalidated rather than Timeout?
                             accept(null, new Timeout(txnId, route.homeKey()));
@@ -174,10 +174,10 @@ public class Coordinate extends AsyncFuture<Result> implements Callback<PreAccep
             }
             else
             {
-                node.withEpoch(executeAt.epoch, () -> {
+                node.withEpoch(executeAt.epoch(), () -> {
                     Topologies topologies = tracker.topologies();
-                    if (executeAt.epoch > txnId.epoch)
-                        topologies = node.topology().withUnsyncedEpochs(route, txnId.epoch, executeAt.epoch);
+                    if (executeAt.epoch() > txnId.epoch())
+                        topologies = node.topology().withUnsyncedEpochs(route, txnId.epoch(), executeAt.epoch());
                     Propose.propose(node, topologies, Ballot.ZERO, txnId, txn, route, executeAt, deps, this);
                 });
             }
diff --git a/accord-core/src/main/java/accord/coordinate/Execute.java b/accord-core/src/main/java/accord/coordinate/Execute.java
index 03ee66c..11464e3 100644
--- a/accord-core/src/main/java/accord/coordinate/Execute.java
+++ b/accord-core/src/main/java/accord/coordinate/Execute.java
@@ -50,13 +50,13 @@ class Execute extends ReadCoordinator<ReadReply>
 
     private Execute(Node node, TxnId txnId, Txn txn, FullRoute<?> route, Seekables<?, ?> readScope, Timestamp executeAt, Deps deps, BiConsumer<Result, Throwable> callback)
     {
-        super(node, node.topology().forEpoch(readScope.toUnseekables(), executeAt.epoch), txnId);
+        super(node, node.topology().forEpoch(readScope.toUnseekables(), executeAt.epoch()), txnId);
         this.txn = txn;
         this.route = route;
         this.readScope = readScope;
         this.executeAt = executeAt;
         this.deps = deps;
-        this.applyTo = node.topology().forEpoch(route, executeAt.epoch);
+        this.applyTo = node.topology().forEpoch(route, executeAt.epoch());
         this.callback = callback;
     }
 
@@ -64,8 +64,8 @@ class Execute extends ReadCoordinator<ReadReply>
     {
         if (txn.read().keys().isEmpty())
         {
-            Topologies sendTo = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
-            Topologies applyTo = node.topology().forEpoch(route, executeAt.epoch);
+            Topologies sendTo = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
+            Topologies applyTo = node.topology().forEpoch(route, executeAt.epoch());
             Result result = txn.result(txnId, null);
             Persist.persist(node, sendTo, applyTo, txnId, route, txn, executeAt, deps, txn.execute(executeAt, null), result);
             callback.accept(result, null);
@@ -112,8 +112,8 @@ class Execute extends ReadCoordinator<ReadReply>
                 return Action.Abort;
             case NotCommitted:
                 // the replica may be missing the original commit, or the additional commit, so send everything
-                Topologies topology = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
-                Topology coordinateTopology = topology.forEpoch(txnId.epoch);
+                Topologies topology = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
+                Topology coordinateTopology = topology.forEpoch(txnId.epoch());
                 node.send(from, new Commit(Maximal, from, coordinateTopology, topology, txnId, txn, route, readScope, executeAt, deps, false));
                 // also try sending a read command to another replica, in case they're ready to serve a response
                 return Action.TryAlternative;
@@ -132,7 +132,7 @@ class Execute extends ReadCoordinator<ReadReply>
             Result result = txn.result(txnId, data);
             callback.accept(result, null);
             // avoid re-calculating topologies if it is unchanged
-            Topologies sendTo = txnId.epoch == executeAt.epoch ? applyTo : node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
+            Topologies sendTo = txnId.epoch() == executeAt.epoch() ? applyTo : node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
             Persist.persist(node, sendTo, applyTo, txnId, route, txn, executeAt, deps, txn.execute(executeAt, data), result);
         }
         else
diff --git a/accord-core/src/main/java/accord/coordinate/FetchData.java b/accord-core/src/main/java/accord/coordinate/FetchData.java
index 1bdba70..688e8eb 100644
--- a/accord-core/src/main/java/accord/coordinate/FetchData.java
+++ b/accord-core/src/main/java/accord/coordinate/FetchData.java
@@ -36,7 +36,7 @@ public class FetchData
     public static Object fetch(Known fetch, Node node, TxnId txnId, Route<?> route, @Nullable Timestamp executeAt, long untilLocalEpoch, BiConsumer<Known, Throwable> callback)
     {
         Invariants.checkArgument(node.topology().hasEpoch(untilLocalEpoch));
-        Ranges ranges = node.topology().localRangesForEpochs(txnId.epoch, untilLocalEpoch);
+        Ranges ranges = node.topology().localRangesForEpochs(txnId.epoch(), untilLocalEpoch);
         if (!route.covers(ranges))
         {
             return fetchWithHomeKey(fetch, node, txnId, route.homeKey(), untilLocalEpoch, callback);
@@ -69,13 +69,13 @@ public class FetchData
 
     public static Object fetch(Known fetch, Node node, TxnId txnId, FullRoute<?> route, @Nullable Timestamp executeAt, long untilLocalEpoch, BiConsumer<Known, Throwable> callback)
     {
-        Ranges ranges = node.topology().localRangesForEpochs(txnId.epoch, untilLocalEpoch);
+        Ranges ranges = node.topology().localRangesForEpochs(txnId.epoch(), untilLocalEpoch);
         return fetchInternal(ranges, fetch, node, txnId, route.sliceStrict(ranges), executeAt, untilLocalEpoch, callback);
     }
 
     private static Object fetchInternal(Ranges ranges, Known target, Node node, TxnId txnId, PartialRoute<?> route, @Nullable Timestamp executeAt, long untilLocalEpoch, BiConsumer<Known, Throwable> callback)
     {
-        long srcEpoch = executeAt == null || target.epoch() == Coordination ? txnId.epoch : executeAt.epoch;
+        long srcEpoch = executeAt == null || target.epoch() == Coordination ? txnId.epoch() : executeAt.epoch();
         if (!node.topology().hasEpoch(srcEpoch))
             return node.topology().awaitEpoch(srcEpoch).map(ignore -> fetchInternal(ranges, target, node, txnId, route, executeAt, untilLocalEpoch, callback));
 
@@ -90,7 +90,7 @@ public class FetchData
                 Known sufficientFor = ok.sufficientFor(fetch);
                 // if we discover the executeAt as part of this action, use that to decide if we requested enough info
                 Timestamp exec = executeAt != null ? executeAt : ok.saveStatus.known.executeAt.isDecisionKnown() ? ok.executeAt : null;
-                if (sufficientFor.outcome == OutcomeKnown && (exec == null || untilLocalEpoch < exec.epoch))
+                if (sufficientFor.outcome == OutcomeKnown && (exec == null || untilLocalEpoch < exec.epoch()))
                     sufficientFor = sufficientFor.with(OutcomeUnknown);
                 callback.accept(sufficientFor, null);
             }
diff --git a/accord-core/src/main/java/accord/coordinate/FindHomeKey.java b/accord-core/src/main/java/accord/coordinate/FindHomeKey.java
index 1f938c3..4d93a18 100644
--- a/accord-core/src/main/java/accord/coordinate/FindHomeKey.java
+++ b/accord-core/src/main/java/accord/coordinate/FindHomeKey.java
@@ -17,7 +17,7 @@ public class FindHomeKey extends CheckShards
     final BiConsumer<RoutingKey, Throwable> callback;
     FindHomeKey(Node node, TxnId txnId, Unseekables<?, ?> unseekables, BiConsumer<RoutingKey, Throwable> callback)
     {
-        super(node, txnId, unseekables, txnId.epoch, IncludeInfo.No);
+        super(node, txnId, unseekables, txnId.epoch(), IncludeInfo.No);
         this.callback = callback;
     }
 
diff --git a/accord-core/src/main/java/accord/coordinate/FindRoute.java b/accord-core/src/main/java/accord/coordinate/FindRoute.java
index 82b116e..8ad9e37 100644
--- a/accord-core/src/main/java/accord/coordinate/FindRoute.java
+++ b/accord-core/src/main/java/accord/coordinate/FindRoute.java
@@ -37,7 +37,7 @@ public class FindRoute extends CheckShards
     final BiConsumer<Result, Throwable> callback;
     FindRoute(Node node, TxnId txnId, RoutingKey homeKey, BiConsumer<Result, Throwable> callback)
     {
-        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch, IncludeInfo.Route);
+        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch(), IncludeInfo.Route);
         this.callback = callback;
     }
 
diff --git a/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java b/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
index 79fac8b..4e4275b 100644
--- a/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
+++ b/accord-core/src/main/java/accord/coordinate/InformHomeOfTxn.java
@@ -49,8 +49,8 @@ public class InformHomeOfTxn extends AsyncFuture<Void> implements Callback<Simpl
 
     public static Future<Void> inform(Node node, TxnId txnId, RoutingKey homeKey)
     {
-        return node.withEpoch(txnId.epoch, () -> {
-            Shard homeShard = node.topology().forEpoch(homeKey, txnId.epoch);
+        return node.withEpoch(txnId.epoch(), () -> {
+            Shard homeShard = node.topology().forEpoch(homeKey, txnId.epoch());
             InformHomeOfTxn inform = new InformHomeOfTxn(txnId, homeKey, homeShard);
             node.send(homeShard.nodes, new InformOfTxnId(txnId, homeKey), inform);
             return inform;
diff --git a/accord-core/src/main/java/accord/coordinate/Invalidate.java b/accord-core/src/main/java/accord/coordinate/Invalidate.java
index 1ed1b75..e7c8cc8 100644
--- a/accord-core/src/main/java/accord/coordinate/Invalidate.java
+++ b/accord-core/src/main/java/accord/coordinate/Invalidate.java
@@ -68,7 +68,7 @@ public class Invalidate implements Callback<InvalidateReply>
         this.txnId = txnId;
         this.transitivelyInvokedByPriorInvalidation = transitivelyInvokedByPriorInvalidation;
         this.invalidateWith = invalidateWith;
-        Topologies topologies = node.topology().forEpoch(invalidateWith, txnId.epoch);
+        Topologies topologies = node.topology().forEpoch(invalidateWith, txnId.epoch());
         this.tracker = new InvalidationTracker(topologies);
     }
 
@@ -195,7 +195,7 @@ public class Invalidate implements Callback<InvalidateReply>
                         Preconditions.checkState(maxReply.status.hasBeen(Accepted) || tracker.all(InvalidationShardTracker::isPromised));
                         // if we included the home shard, and we have either a recoverable status OR have not rejected the fast path,
                         // we must have at least one response that should contain the Route
-                        if (invalidateWith.contains(homeKey) && tracker.isPromisedForKey(homeKey, txnId.epoch))
+                        if (invalidateWith.contains(homeKey) && tracker.isPromisedForKey(homeKey, txnId.epoch()))
                             throw new IllegalStateException("Received replies from a node that must have known the route, but that did not include it");
 
                         // if < Accepted, we should have short-circuited to invalidation above. This guarantees no Invaldate/Recover loop, as any later status will forbid invoking Invalidate
diff --git a/accord-core/src/main/java/accord/coordinate/MaybeRecover.java b/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
index 1522ec4..634d588 100644
--- a/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
+++ b/accord-core/src/main/java/accord/coordinate/MaybeRecover.java
@@ -47,7 +47,7 @@ public class MaybeRecover extends CheckShards
     MaybeRecover(Node node, TxnId txnId, RoutingKey homeKey, @Nullable Route<?> route, ProgressToken prevProgress, BiConsumer<Outcome, Throwable> callback)
     {
         // we only want to enquire with the home shard, but we prefer maximal route information for running Invalidation against, if necessary
-        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch, IncludeInfo.Route);
+        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch(), IncludeInfo.Route);
         this.homeKey = homeKey;
         this.route = route;
         this.prevProgress = prevProgress;
diff --git a/accord-core/src/main/java/accord/coordinate/Persist.java b/accord-core/src/main/java/accord/coordinate/Persist.java
index a7daec9..63b84ee 100644
--- a/accord-core/src/main/java/accord/coordinate/Persist.java
+++ b/accord-core/src/main/java/accord/coordinate/Persist.java
@@ -56,15 +56,15 @@ public class Persist implements Callback<ApplyReply>
     public static void persist(Node node, Topologies sendTo, Topologies applyTo, TxnId txnId, FullRoute<?> route, Txn txn, Timestamp executeAt, Deps deps, Writes writes, Result result)
     {
         Persist persist = new Persist(node, applyTo, txnId, route, txn, executeAt, deps);
-        node.send(sendTo.nodes(), to -> new Apply(to, sendTo, applyTo, executeAt.epoch, txnId, route, executeAt, deps, writes, result), persist);
+        node.send(sendTo.nodes(), to -> new Apply(to, sendTo, applyTo, executeAt.epoch(), txnId, route, executeAt, deps, writes, result), persist);
     }
 
     public static void persistAndCommit(Node node, TxnId txnId, FullRoute<?> route, Txn txn, Timestamp executeAt, Deps deps, Writes writes, Result result)
     {
-        Topologies sendTo = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
-        Topologies applyTo = node.topology().forEpoch(route, executeAt.epoch);
+        Topologies sendTo = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
+        Topologies applyTo = node.topology().forEpoch(route, executeAt.epoch());
         Persist persist = new Persist(node, sendTo, txnId, route, txn, executeAt, deps);
-        node.send(sendTo.nodes(), to -> new Apply(to, sendTo, applyTo, executeAt.epoch, txnId, route, executeAt, deps, writes, result), persist);
+        node.send(sendTo.nodes(), to -> new Apply(to, sendTo, applyTo, executeAt.epoch(), txnId, route, executeAt, deps, writes, result), persist);
     }
 
     private Persist(Node node, Topologies topologies, TxnId txnId, FullRoute<?> route, Txn txn, Timestamp executeAt, Deps deps)
@@ -93,21 +93,21 @@ public class Persist implements Callback<ApplyReply>
                     if (!isDone)
                     {
                         // TODO (low priority, consider, efficiency): send to non-home replicas also, so they may clear their log more easily?
-                        Shard homeShard = node.topology().forEpochIfKnown(route.homeKey(), txnId.epoch);
+                        Shard homeShard = node.topology().forEpochIfKnown(route.homeKey(), txnId.epoch());
                         node.send(homeShard, new InformHomeDurable(txnId, route.homeKey(), executeAt, Durable, persistedOn));
                         isDone = true;
                     }
                     else if (!tracker.hasInFlight() && !tracker.hasFailures())
                     {
-                        Shard homeShard = node.topology().forEpochIfKnown(route.homeKey(), txnId.epoch);
+                        Shard homeShard = node.topology().forEpochIfKnown(route.homeKey(), txnId.epoch());
                         node.send(homeShard, new InformHomeDurable(txnId, route.homeKey(), executeAt, Universal, persistedOn));
                     }
                 }
                 break;
             case Insufficient:
-                Topologies topologies = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
+                Topologies topologies = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
                 // TODO (easy, cleanup): use static method in Commit
-                node.send(from, new Commit(Kind.Maximal, from, topologies.forEpoch(txnId.epoch), topologies, txnId, txn, route, null, executeAt, deps, false));
+                node.send(from, new Commit(Kind.Maximal, from, topologies.forEpoch(txnId.epoch()), topologies, txnId, txn, route, null, executeAt, deps, false));
         }
     }
 
diff --git a/accord-core/src/main/java/accord/coordinate/Propose.java b/accord-core/src/main/java/accord/coordinate/Propose.java
index 6209f9d..3b75512 100644
--- a/accord-core/src/main/java/accord/coordinate/Propose.java
+++ b/accord-core/src/main/java/accord/coordinate/Propose.java
@@ -152,7 +152,7 @@ class Propose implements Callback<AcceptReply>
 
         public static Invalidate proposeInvalidate(Node node, Ballot ballot, TxnId txnId, RoutingKey invalidateWithKey, BiConsumer<Void, Throwable> callback)
         {
-            Shard shard = node.topology().forEpochIfKnown(invalidateWithKey, txnId.epoch);
+            Shard shard = node.topology().forEpochIfKnown(invalidateWithKey, txnId.epoch());
             Invalidate invalidate = new Invalidate(node, shard, ballot, txnId, invalidateWithKey, callback);
             node.send(shard.nodes, to -> new Accept.Invalidate(ballot, txnId, invalidateWithKey), invalidate);
             return invalidate;
diff --git a/accord-core/src/main/java/accord/coordinate/Recover.java b/accord-core/src/main/java/accord/coordinate/Recover.java
index 9f8e80c..3ad5d67 100644
--- a/accord-core/src/main/java/accord/coordinate/Recover.java
+++ b/accord-core/src/main/java/accord/coordinate/Recover.java
@@ -63,7 +63,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
 
         AwaitCommit(Node node, TxnId txnId, Unseekables<?, ?> unseekables)
         {
-            Topology topology = node.topology().globalForEpoch(txnId.epoch).forSelection(unseekables);
+            Topology topology = node.topology().globalForEpoch(txnId.epoch()).forSelection(unseekables);
             this.tracker = new QuorumTracker(new Topologies.Single(node.topology().sorter(), topology));
             node.send(topology.nodes(), to -> new WaitOnCommit(to, topology, txnId, unseekables), this);
         }
@@ -132,7 +132,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
         this.txn = txn;
         this.route = route;
         this.callback = callback;
-        assert topologies.oldestEpoch() == topologies.currentEpoch() && topologies.currentEpoch() == txnId.epoch;
+        assert topologies.oldestEpoch() == topologies.currentEpoch() && topologies.currentEpoch() == txnId.epoch();
         this.tracker = new RecoveryTracker(topologies);
     }
 
@@ -147,7 +147,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
 
     public static Recover recover(Node node, TxnId txnId, Txn txn, FullRoute<?> route, BiConsumer<Outcome, Throwable> callback)
     {
-        return recover(node, txnId, txn, route, callback, node.topology().forEpoch(route, txnId.epoch));
+        return recover(node, txnId, txn, route, callback, node.topology().forEpoch(route, txnId.epoch()));
     }
 
     public static Recover recover(Node node, TxnId txnId, Txn txn, FullRoute<?> route, BiConsumer<Outcome, Throwable> callback, Topologies topologies)
@@ -158,7 +158,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
 
     public static Recover recover(Node node, Ballot ballot, TxnId txnId, Txn txn, FullRoute<?> route, BiConsumer<Outcome, Throwable> callback)
     {
-        return recover(node, ballot, txnId, txn, route, callback, node.topology().forEpoch(route, txnId.epoch));
+        return recover(node, ballot, txnId, txn, route, callback, node.topology().forEpoch(route, txnId.epoch()));
     }
 
     public static Recover recover(Node node, Ballot ballot, TxnId txnId, Txn txn, FullRoute<?> route, BiConsumer<Outcome, Throwable> callback, Topologies topologies)
@@ -212,7 +212,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
                 case Applied:
                 case PreApplied:
                     // TODO (desired, efficiency): in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
-                    node.withEpoch(executeAt.epoch, () -> {
+                    node.withEpoch(executeAt.epoch(), () -> {
                         CollectDeps.withDeps(node, txnId, route, txn, acceptOrCommit.executeAt, (deps, fail) -> {
                             if (fail != null)
                             {
@@ -232,7 +232,7 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
                 case PreCommitted:
                 case Committed:
                     // TODO (desired, efficiency): in some cases we can use the deps we already have (e.g. if we have a quorum of Committed responses)
-                    node.withEpoch(executeAt.epoch, () -> {
+                    node.withEpoch(executeAt.epoch(), () -> {
                         CollectDeps.withDeps(node, txnId, route, txn, executeAt, (deps, fail) -> {
                             if (fail != null) accept(null, fail);
                             else Execute.execute(node, txnId, txn, route, acceptOrCommit.executeAt, deps, this);
@@ -294,14 +294,14 @@ public class Recover implements Callback<RecoverReply>, BiConsumer<Result, Throw
     private void commitInvalidate()
     {
         Timestamp invalidateUntil = recoverOks.stream().map(ok -> ok.executeAt).reduce(txnId, Timestamp::max);
-        node.withEpoch(invalidateUntil.epoch, () -> Commit.Invalidate.commitInvalidate(node, txnId, route, invalidateUntil));
+        node.withEpoch(invalidateUntil.epoch(), () -> Commit.Invalidate.commitInvalidate(node, txnId, route, invalidateUntil));
         isDone = true;
         callback.accept(ProgressToken.INVALIDATED, null);
     }
 
     private void propose(Timestamp executeAt, Deps deps)
     {
-        node.withEpoch(executeAt.epoch, () -> Propose.propose(node, ballot, txnId, txn, route, executeAt, deps, this));
+        node.withEpoch(executeAt.epoch(), () -> Propose.propose(node, ballot, txnId, txn, route, executeAt, deps, this));
     }
 
     private Deps mergeDeps()
diff --git a/accord-core/src/main/java/accord/coordinate/RecoverWithHomeKey.java b/accord-core/src/main/java/accord/coordinate/RecoverWithHomeKey.java
index 1ea6856..dabd86e 100644
--- a/accord-core/src/main/java/accord/coordinate/RecoverWithHomeKey.java
+++ b/accord-core/src/main/java/accord/coordinate/RecoverWithHomeKey.java
@@ -26,7 +26,7 @@ public class RecoverWithHomeKey extends CheckShards implements BiConsumer<Object
 
     RecoverWithHomeKey(Node node, TxnId txnId, RoutingKey homeKey, Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
     {
-        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch, IncludeInfo.Route);
+        super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch(), IncludeInfo.Route);
         this.witnessedByInvalidation = witnessedByInvalidation;
         // if witnessedByInvalidation == AcceptedInvalidate then we cannot assume its definition was known, and our comparison with the status is invalid
         Preconditions.checkState(witnessedByInvalidation != Status.AcceptedInvalidate);
diff --git a/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java b/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
index 775f1d7..9dfb0b1 100644
--- a/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
+++ b/accord-core/src/main/java/accord/coordinate/RecoverWithRoute.java
@@ -31,7 +31,7 @@ public class RecoverWithRoute extends CheckShards
 
     private RecoverWithRoute(Node node, Topologies topologies, @Nullable Ballot promisedBallot, TxnId txnId, FullRoute<?> route, Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
     {
-        super(node, txnId, route, txnId.epoch, IncludeInfo.All);
+        super(node, txnId, route, txnId.epoch(), IncludeInfo.All);
         // if witnessedByInvalidation == AcceptedInvalidate then we cannot assume its definition was known, and our comparison with the status is invalid
         Invariants.checkState(witnessedByInvalidation != Status.AcceptedInvalidate);
         // if witnessedByInvalidation == Invalidated we should anyway not be recovering
@@ -40,12 +40,12 @@ public class RecoverWithRoute extends CheckShards
         this.route = route;
         this.callback = callback;
         this.witnessedByInvalidation = witnessedByInvalidation;
-        assert topologies.oldestEpoch() == topologies.currentEpoch() && topologies.currentEpoch() == txnId.epoch;
+        assert topologies.oldestEpoch() == topologies.currentEpoch() && topologies.currentEpoch() == txnId.epoch();
     }
 
     public static RecoverWithRoute recover(Node node, TxnId txnId, FullRoute<?> route, @Nullable Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
     {
-        return recover(node, node.topology().forEpoch(route, txnId.epoch), txnId, route, witnessedByInvalidation, callback);
+        return recover(node, node.topology().forEpoch(route, txnId.epoch()), txnId, route, witnessedByInvalidation, callback);
     }
 
     public static RecoverWithRoute recover(Node node, Topologies topologies, TxnId txnId, FullRoute<?> route, @Nullable Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
@@ -55,7 +55,7 @@ public class RecoverWithRoute extends CheckShards
 
     public static RecoverWithRoute recover(Node node, @Nullable Ballot promisedBallot, TxnId txnId, FullRoute<?> route, @Nullable Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
     {
-        return recover(node, node.topology().forEpoch(route, txnId.epoch), promisedBallot, txnId, route, witnessedByInvalidation, callback);
+        return recover(node, node.topology().forEpoch(route, txnId.epoch()), promisedBallot, txnId, route, witnessedByInvalidation, callback);
     }
 
     public static RecoverWithRoute recover(Node node, Topologies topologies, Ballot ballot, TxnId txnId, FullRoute<?> route, Status witnessedByInvalidation, BiConsumer<Outcome, Throwable> callback)
@@ -79,7 +79,7 @@ public class RecoverWithRoute extends CheckShards
     @Override
     protected boolean isSufficient(Id from, CheckStatusOk ok)
     {
-        Ranges rangesForNode = topologies().forEpoch(txnId.epoch).rangesForNode(from);
+        Ranges rangesForNode = topologies().forEpoch(txnId.epoch()).rangesForNode(from);
         PartialRoute<?> route = this.route.slice(rangesForNode);
         return isSufficient(route, ok);
     }
@@ -141,7 +141,7 @@ public class RecoverWithRoute extends CheckShards
                 if (known.deps.isDecisionKnown())
                 {
                     Deps deps = merged.committedDeps.reconstitute(route());
-                    node.withEpoch(merged.executeAt.epoch, () -> {
+                    node.withEpoch(merged.executeAt.epoch(), () -> {
                         Persist.persistAndCommit(node, txnId, route(), txn, merged.executeAt, deps, merged.writes, merged.result);
                     });
                     callback.accept(APPLIED, null);
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommand.java b/accord-core/src/main/java/accord/impl/InMemoryCommand.java
index 79a5ec7..d72b574 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommand.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommand.java
@@ -24,7 +24,6 @@ import accord.local.*;
 import accord.local.Status.Durability;
 import accord.local.Status.Known;
 import accord.primitives.*;
-import accord.primitives.Txn.Kind;
 
 import javax.annotation.Nullable;
 import java.util.*;
@@ -40,7 +39,6 @@ public class InMemoryCommand extends Command
     private Route<?> route;
     private RoutingKey homeKey, progressKey;
     private PartialTxn partialTxn;
-    private Kind kind;
     private Ballot promised = Ballot.ZERO, accepted = Ballot.ZERO;
     private Timestamp executeAt;
     private @Nullable PartialDeps partialDeps = null;
@@ -104,18 +102,6 @@ public class InMemoryCommand extends Command
         return homeKey;
     }
 
-    @Override
-    public Kind kind()
-    {
-        return kind;
-    }
-
-    @Override
-    public void setKind(Kind kind)
-    {
-        this.kind = kind;
-    }
-
     @Override
     protected void setHomeKey(RoutingKey key)
     {
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
index f112d0c..7977d2c 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
@@ -29,7 +29,6 @@ import accord.impl.InMemoryCommandStore.Synchronized.SynchronizedState;
 import accord.local.Command;
 import accord.local.CommandsForKey;
 import accord.local.CommandListener;
-import accord.local.Node;
 import accord.local.NodeTimeService;
 import accord.local.PreLoadContext;
 import accord.local.SafeCommandStore;
@@ -161,9 +160,9 @@ public class InMemoryCommandStore
         @Override
         public Timestamp preaccept(TxnId txnId, Seekables<?, ?> keys)
         {
-            Timestamp max = maxConflict(keys, ranges().at(txnId.epoch));
+            Timestamp max = maxConflict(keys, ranges().at(txnId.epoch()));
             long epoch = latestEpoch();
-            if (txnId.compareTo(max) > 0 && txnId.epoch >= epoch && !agent.isExpired(txnId, time.now()))
+            if (txnId.compareTo(max) > 0 && txnId.epoch() >= epoch && !agent.isExpired(txnId, time.now()))
                 return txnId;
 
             return time.uniqueNow(max);
@@ -187,8 +186,8 @@ public class InMemoryCommandStore
 
         public void forEpochCommands(Ranges ranges, long epoch, Consumer<Command> consumer)
         {
-            Timestamp minTimestamp = new Timestamp(epoch, Long.MIN_VALUE, Integer.MIN_VALUE, Node.Id.NONE);
-            Timestamp maxTimestamp = new Timestamp(epoch, Long.MAX_VALUE, Integer.MAX_VALUE, Node.Id.MAX);
+            Timestamp minTimestamp = Timestamp.minForEpoch(epoch);
+            Timestamp maxTimestamp = Timestamp.maxForEpoch(epoch);
             for (Range range : ranges)
             {
                 Iterable<InMemoryCommandsForKey> rangeCommands = commandsForKey.subMap(
@@ -205,8 +204,8 @@ public class InMemoryCommandStore
 
         public void forCommittedInEpoch(Ranges ranges, long epoch, Consumer<Command> consumer)
         {
-            Timestamp minTimestamp = new Timestamp(epoch, Long.MIN_VALUE, Integer.MIN_VALUE, Node.Id.NONE);
-            Timestamp maxTimestamp = new Timestamp(epoch, Long.MAX_VALUE, Integer.MAX_VALUE, Node.Id.MAX);
+            Timestamp minTimestamp = Timestamp.minForEpoch(epoch);
+            Timestamp maxTimestamp = Timestamp.maxForEpoch(epoch);
             for (Range range : ranges)
             {
                 Iterable<InMemoryCommandsForKey> rangeCommands = commandsForKey.subMap(range.start(),
@@ -262,7 +261,7 @@ public class InMemoryCommandStore
 
         public void forEach(Routable keyOrRange, Ranges slice, Consumer<CommandsForKey> forEach)
         {
-            switch (keyOrRange.kind())
+            switch (keyOrRange.domain())
             {
                 default: throw new AssertionError();
                 case Key:
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandsForKey.java b/accord-core/src/main/java/accord/impl/InMemoryCommandsForKey.java
index 11bffff..8287f45 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandsForKey.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandsForKey.java
@@ -37,7 +37,6 @@ import static accord.local.CommandsForKey.CommandTimeseries.TestDep.*;
 import static accord.local.CommandsForKey.CommandTimeseries.TestKind.RorWs;
 import static accord.local.Status.KnownDeps.DepsUnknown;
 import static accord.local.Status.PreAccepted;
-import static accord.primitives.Txn.Kind.WRITE;
 
 public class InMemoryCommandsForKey extends CommandsForKey
 {
@@ -78,7 +77,7 @@ public class InMemoryCommandsForKey extends CommandsForKey
         public Stream<T> before(@Nonnull Timestamp timestamp, @Nonnull TestKind testKind, @Nonnull TestDep testDep, @Nullable TxnId depId, @Nonnull TestStatus testStatus, @Nullable Status status)
         {
             return commands.headMap(timestamp, false).values().stream()
-                    .filter(cmd -> testKind == RorWs || cmd.kind() == WRITE)
+                    .filter(cmd -> testKind == RorWs || cmd.txnId().isWrite())
                     // If we don't have any dependencies, we treat a dependency filter as a mismatch
                     .filter(cmd -> testDep == ANY_DEPS || (cmd.known().deps != DepsUnknown && (cmd.partialDeps().contains(depId) ^ (testDep == WITHOUT))))
                     .filter(cmd -> TestStatus.test(cmd.status(), testStatus, status))
@@ -89,7 +88,7 @@ public class InMemoryCommandsForKey extends CommandsForKey
         public Stream<T> after(@Nonnull Timestamp timestamp, @Nonnull TestKind testKind, @Nonnull TestDep testDep, @Nullable TxnId depId, @Nonnull TestStatus testStatus, @Nullable Status status)
         {
             return commands.tailMap(timestamp, false).values().stream()
-                    .filter(cmd -> testKind == RorWs || cmd.kind() == WRITE)
+                    .filter(cmd -> testKind == RorWs || cmd.txnId().isWrite())
                     // If we don't have any dependencies, we treat a dependency filter as a mismatch
                     .filter(cmd -> testDep == ANY_DEPS || (cmd.known().deps != DepsUnknown && (cmd.partialDeps().contains(depId) ^ (testDep == WITHOUT))))
                     .filter(cmd -> TestStatus.test(cmd.status(), testStatus, status))
diff --git a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
index 060af15..7fff500 100644
--- a/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
+++ b/accord-core/src/main/java/accord/impl/SimpleProgressLog.java
@@ -223,7 +223,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                                 // must also be committed, as at the time of writing we do not guarantee dissemination of Commit
                                 // records to the home shard, so we only know the executeAt shards will have witnessed this
                                 // if the home shard is at an earlier phase, it must run recovery
-                                long epoch = command.executeAt().epoch;
+                                long epoch = command.executeAt().epoch();
                                 node.withEpoch(epoch, () -> debugInvestigating = FetchData.fetch(PreApplied.minKnown, node, txnId, command.route(), epoch, (success, fail) -> {
                                     commandStore.execute(PreLoadContext.empty(), ignore -> {
                                         // should have found enough information to apply the result, but in case we did not reset progress
@@ -235,7 +235,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                             else
                             {
                                 RoutingKey homeKey = command.homeKey();
-                                node.withEpoch(txnId.epoch, () -> {
+                                node.withEpoch(txnId.epoch(), () -> {
 
                                     Future<? extends Outcome> recover = node.maybeRecover(txnId, homeKey, command.route(), token);
                                     recover.addCallback((success, fail) -> {
@@ -349,10 +349,10 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     if (!isFullRoute(command.route()))
                         return false;
 
-                    if (!node.topology().hasEpoch(command.executeAt().epoch))
+                    if (!node.topology().hasEpoch(command.executeAt().epoch()))
                         return false;
 
-                    Topologies topology = node.topology().preciseEpochs(command.route(), command.txnId().epoch, command.executeAt().epoch);
+                    Topologies topology = node.topology().preciseEpochs(command.route(), command.txnId().epoch(), command.executeAt().epoch());
                     notAwareOfDurability = topology.copyOfNodes();
                     notPersisted = topology.copyOfNodes();
                     if (whenReady != null)
@@ -432,7 +432,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     FullRoute<?> route = Route.castToFullRoute(command.route());
                     Timestamp executeAt = command.executeAt();
                     investigating = new CoordinateAwareness();
-                    Topologies topologies = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
+                    Topologies topologies = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
                     node.send(notAwareOfDurability, to -> new InformDurable(to, topologies, route, txnId, executeAt, Durable), investigating);
                 }
 
@@ -481,7 +481,7 @@ public class SimpleProgressLog implements ProgressLog.Factory
                     setProgress(Investigating);
                     // first make sure we have enough information to obtain the command locally
                     Timestamp executeAt = command.hasBeen(PreCommitted) ? command.executeAt() : null;
-                    long srcEpoch = (executeAt != null ? executeAt : txnId).epoch;
+                    long srcEpoch = (executeAt != null ? executeAt : txnId).epoch();
                     // TODO (desired, consider): compute fromEpoch, the epoch we already have this txn replicated until
                     long toEpoch = Math.max(srcEpoch, node.topology().epoch());
                     Unseekables<?, ?> someKeys = unseekables(command);
diff --git a/accord-core/src/main/java/accord/local/Command.java b/accord-core/src/main/java/accord/local/Command.java
index 3915411..4f7697f 100644
--- a/accord-core/src/main/java/accord/local/Command.java
+++ b/accord-core/src/main/java/accord/local/Command.java
@@ -72,9 +72,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
     public abstract TxnId txnId();
 
-    public abstract Kind kind();
-    public abstract void setKind(Kind kind);
-
     // TODO (desirable, API consistency): should any of these calls be replaced by corresponding known() registers?
     public boolean hasBeen(Status status)
     {
@@ -295,12 +292,12 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
             return AcceptOutcome.Redundant;
         }
 
-        if (known().isDefinitionKnown() && !kind().equals(kind))
+        if (known().isDefinitionKnown() && txnId().rw() != kind)
             throw new IllegalArgumentException("Transaction kind is different to the definition we have already received");
 
         TxnId txnId = txnId();
         Ranges coordinateRanges = coordinateRanges(safeStore);
-        Ranges acceptRanges = txnId.epoch == executeAt.epoch ? coordinateRanges : safeStore.ranges().between(txnId.epoch, executeAt.epoch);
+        Ranges acceptRanges = txnId.epoch() == executeAt.epoch() ? coordinateRanges : safeStore.ranges().between(txnId.epoch(), executeAt.epoch());
         ProgressShard shard = progressShard(safeStore, route, progressKey, coordinateRanges);
 
         if (!validate(coordinateRanges, Ranges.EMPTY, shard, route, Ignore, null, Ignore, partialDeps, Set))
@@ -312,7 +309,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         setExecuteAt(executeAt);
         setPromised(ballot);
         setAccepted(ballot);
-        setKind(kind);
         set(safeStore, coordinateRanges, Ranges.EMPTY, shard, route, null, Ignore, partialDeps, Set);
         switch (status())
         {
@@ -410,7 +406,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
     protected void populateWaitingOn(SafeCommandStore safeStore)
     {
-        Ranges ranges = safeStore.ranges().since(executeAt().epoch);
+        Ranges ranges = safeStore.ranges().since(executeAt().epoch());
         if (ranges != null) {
             partialDeps().forEachOn(ranges, txnId -> {
                 Command command = safeStore.ifLoaded(txnId);
@@ -492,7 +488,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         Ranges executeRanges = executeRanges(safeStore, executeAt);
         if (untilEpoch < safeStore.latestEpoch())
         {
-            Ranges expectedRanges = safeStore.ranges().between(executeAt.epoch, untilEpoch);
+            Ranges expectedRanges = safeStore.ranges().between(executeAt.epoch(), untilEpoch);
             Invariants.checkState(expectedRanges.containsAll(executeRanges));
         }
         ProgressShard shard = progressShard(safeStore, route, coordinateRanges);
@@ -830,7 +826,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
             setHomeKey(homeKey);
             // TODO (low priority, safety): if we're processed on a node that does not know the latest epoch,
             //      do we guarantee the home key calculation is unchanged since the prior epoch?
-            if (progressKey() == null && owns(safeStore, txnId().epoch, homeKey))
+            if (progressKey() == null && owns(safeStore, txnId().epoch(), homeKey))
                 progressKey(homeKey);
         }
         else if (!this.homeKey().equals(homeKey))
@@ -892,7 +888,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (progressKey == NO_PROGRESS_KEY)
             return No;
 
-        Ranges coordinateRanges = safeStore.ranges().at(txnId().epoch);
+        Ranges coordinateRanges = safeStore.ranges().at(txnId().epoch());
         if (!coordinateRanges.contains(progressKey))
             return No;
 
@@ -901,12 +897,12 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
     private Ranges coordinateRanges(SafeCommandStore safeStore)
     {
-        return safeStore.ranges().at(txnId().epoch);
+        return safeStore.ranges().at(txnId().epoch());
     }
 
     private Ranges executeRanges(SafeCommandStore safeStore, Timestamp executeAt)
     {
-        return safeStore.ranges().since(executeAt.epoch);
+        return safeStore.ranges().since(executeAt.epoch());
     }
 
     enum EnsureAction { Ignore, Check, Add, TrySet, Set }
@@ -977,7 +973,7 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
         if (!validate(ensurePartialTxn, existingRanges, additionalRanges, covers(partialTxn()), covers(partialTxn), "txn", partialTxn))
             return false;
 
-        if (partialTxn != null && kind() != null && !kind().equals(partialTxn.kind()))
+        if (partialTxn != null && txnId().rw() != partialTxn.kind())
             throw new IllegalArgumentException("Transaction has different kind to the definition we previously received");
 
         if (shard.isHome() && ensurePartialTxn != Ignore)
@@ -1019,7 +1015,6 @@ public abstract class Command implements CommandListener, BiConsumer<SafeCommand
 
             case Set:
             case TrySet:
-                setKind(partialTxn.kind());
                 setPartialTxn(partialTxn = partialTxn.slice(allRanges, shard.isHome()));
                 // TODO (expected, efficiency): we may register the same ranges more than once
                 safeStore.forEach(partialTxn.keys(), allRanges, forKey -> {
diff --git a/accord-core/src/main/java/accord/local/Node.java b/accord-core/src/main/java/accord/local/Node.java
index 6b1ed4e..9047034 100644
--- a/accord-core/src/main/java/accord/local/Node.java
+++ b/accord-core/src/main/java/accord/local/Node.java
@@ -30,6 +30,7 @@ import java.util.function.Supplier;
 import accord.coordinate.*;
 import accord.messages.*;
 import accord.primitives.*;
+import accord.primitives.Routable.Domain;
 import accord.utils.MapReduceConsume;
 import com.google.common.annotations.VisibleForTesting;
 
@@ -56,6 +57,8 @@ import net.nicoulaj.compilecommand.annotations.Inline;
 import org.apache.cassandra.utils.concurrent.AsyncFuture;
 import org.apache.cassandra.utils.concurrent.Future;
 
+import static accord.primitives.Routable.Domain.Key;
+
 public class Node implements ConfigurationService.Listener, NodeTimeService
 {
     public static class Id implements Comparable<Id>
@@ -131,7 +134,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         this.topology = new TopologyManager(topologySorter, id);
         this.nowSupplier = nowSupplier;
         Topology topology = configService.currentTopology();
-        this.now = new AtomicReference<>(new Timestamp(topology.epoch(), nowSupplier.getAsLong(), 0, id));
+        this.now = new AtomicReference<>(Timestamp.fromValues(topology.epoch(), nowSupplier.getAsLong(), id));
         this.agent = agent;
         this.random = random;
         this.scheduler = scheduler;
@@ -225,10 +228,10 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         return now.updateAndGet(cur -> {
             // TODO (low priority, proof): this diverges from proof; either show isomorphism or make consistent
             long now = nowSupplier.getAsLong();
-            long epoch = Math.max(cur.epoch, topology.epoch());
-            return (now > cur.real)
-                 ? new Timestamp(epoch, now, 0, id)
-                 : new Timestamp(epoch, cur.real, cur.logical + 1, id);
+            long epoch = Math.max(cur.epoch(), topology.epoch());
+            return now > cur.hlc()
+                 ? Timestamp.fromValues(epoch, now, id)
+                 : Timestamp.fromValues(epoch, cur.hlc() + 1, id);
         });
     }
 
@@ -237,8 +240,8 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         if (now.get().compareTo(atLeast) < 0)
             now.accumulateAndGet(atLeast, (current, proposed) -> {
                 long minEpoch = topology.epoch();
-                current = current.withMinEpoch(minEpoch);
-                proposed = proposed.withMinEpoch(minEpoch);
+                current = current.withEpochAtLeast(minEpoch);
+                proposed = proposed.withEpochAtLeast(minEpoch);
                 return proposed.compareTo(current) <= 0 ? current.logicalNext(id) : proposed;
             });
         return uniqueNow();
@@ -256,7 +259,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public Future<Void> forEachLocalSince(PreLoadContext context, Unseekables<?, ?> unseekables, Timestamp since, Consumer<SafeCommandStore> forEach)
     {
-        return commandStores.forEach(context, unseekables, since.epoch, Long.MAX_VALUE, forEach);
+        return commandStores.forEach(context, unseekables, since.epoch(), Long.MAX_VALUE, forEach);
     }
 
     public Future<Void> ifLocal(PreLoadContext context, RoutingKey key, long epoch, Consumer<SafeCommandStore> ifLocal)
@@ -266,7 +269,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public Future<Void> ifLocalSince(PreLoadContext context, RoutingKey key, Timestamp since, Consumer<SafeCommandStore> ifLocal)
     {
-        return commandStores.ifLocal(context, key, since.epoch, Long.MAX_VALUE, ifLocal);
+        return commandStores.ifLocal(context, key, since.epoch(), Long.MAX_VALUE, ifLocal);
     }
 
     public <T> void mapReduceConsumeLocal(TxnRequest<?> request, long minEpoch, long maxEpoch, MapReduceConsume<SafeCommandStore, T> mapReduceConsume)
@@ -353,14 +356,14 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         messageSink.reply(replyingToNode, replyContext, send);
     }
 
-    public TxnId nextTxnId()
+    public TxnId nextTxnId(Txn.Kind rw, Domain domain)
     {
-        return new TxnId(uniqueNow());
+        return new TxnId(uniqueNow(), rw, domain);
     }
 
     public Future<Result> coordinate(Txn txn)
     {
-        return coordinate(nextTxnId(), txn);
+        return coordinate(nextTxnId(txn.kind(), Key), txn);
     }
 
     public Future<Result> coordinate(TxnId txnId, Txn txn)
@@ -368,7 +371,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
         // TODO (desirable, consider): The combination of updating the epoch of the next timestamp with epochs we don't have topologies for,
         //  and requiring preaccept to talk to its topology epoch means that learning of a new epoch via timestamp
         //  (ie not via config service) will halt any new txns from a node until it receives this topology
-        Future<Result> result = withEpoch(txnId.epoch, () -> initiateCoordination(txnId, txn));
+        Future<Result> result = withEpoch(txnId.epoch(), () -> initiateCoordination(txnId, txn));
         coordinating.putIfAbsent(txnId, result);
         result.addCallback((success, fail) -> coordinating.remove(txnId, result));
         return result;
@@ -390,13 +393,13 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     private @Nullable RoutingKey trySelectHomeKey(TxnId txnId, Seekables<?, ?> keysOrRanges)
     {
-        int i = (int)keysOrRanges.findNextIntersection(0, topology().localForEpoch(txnId.epoch).ranges(), 0);
+        int i = (int)keysOrRanges.findNextIntersection(0, topology().localForEpoch(txnId.epoch()).ranges(), 0);
         return i >= 0 ? keysOrRanges.get(i).someIntersectingRoutingKey() : null;
     }
 
     public RoutingKey selectProgressKey(TxnId txnId, Route<?> route, RoutingKey homeKey)
     {
-        return selectProgressKey(txnId.epoch, route, homeKey);
+        return selectProgressKey(txnId.epoch(), route, homeKey);
     }
 
     public RoutingKey selectProgressKey(long epoch, Route<?> route, RoutingKey homeKey)
@@ -414,7 +417,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public RoutingKey trySelectProgressKey(TxnId txnId, Route<?> route, RoutingKey homeKey)
     {
-        return trySelectProgressKey(txnId.epoch, route, homeKey);
+        return trySelectProgressKey(txnId.epoch(), route, homeKey);
     }
 
     public RoutingKey trySelectProgressKey(long epoch, Route<?> route, RoutingKey homeKey)
@@ -435,7 +438,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
 
     public RoutingKey selectRandomHomeKey(TxnId txnId)
     {
-        Ranges ranges = topology().localForEpoch(txnId.epoch).ranges();
+        Ranges ranges = topology().localForEpoch(txnId.epoch()).ranges();
         Range range = ranges.get(ranges.size() == 1 ? 0 : random.nextInt(ranges.size()));
         return range.someIntersectingRoutingKey();
     }
@@ -458,7 +461,7 @@ public class Node implements ConfigurationService.Listener, NodeTimeService
                 return result;
         }
 
-        Future<Outcome> result = withEpoch(txnId.epoch, () -> {
+        Future<Outcome> result = withEpoch(txnId.epoch(), () -> {
             RecoverFuture<Outcome> future = new RecoverFuture<>();
             RecoverWithRoute.recover(this, txnId, route, null, future);
             return future;
diff --git a/accord-core/src/main/java/accord/messages/Accept.java b/accord-core/src/main/java/accord/messages/Accept.java
index 22d2a87..7ea2631 100644
--- a/accord-core/src/main/java/accord/messages/Accept.java
+++ b/accord-core/src/main/java/accord/messages/Accept.java
@@ -92,7 +92,7 @@ public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
                 return new AcceptReply(command.promised());
             case Success:
                 // TODO (desirable, efficiency): we don't need to calculate deps if executeAt == txnId
-                return new AcceptReply(calculatePartialDeps(safeStore, txnId, keys, kind, executeAt, safeStore.ranges().between(minEpoch, executeAt.epoch)));
+                return new AcceptReply(calculatePartialDeps(safeStore, txnId, keys, kind, executeAt, safeStore.ranges().between(minEpoch, executeAt.epoch())));
         }
     }
 
@@ -116,7 +116,7 @@ public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
 
     public void process()
     {
-        node.mapReduceConsumeLocal(this, minEpoch, executeAt.epoch, this);
+        node.mapReduceConsumeLocal(this, minEpoch, executeAt.epoch(), this);
     }
 
     @Override
@@ -222,7 +222,7 @@ public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
 
         public void process()
         {
-            node.mapReduceConsumeLocal(this, someKey, txnId.epoch, this);
+            node.mapReduceConsumeLocal(this, someKey, txnId.epoch(), this);
         }
 
         @Override
@@ -256,7 +256,7 @@ public class Accept extends TxnRequest.WithUnsynced<Accept.AcceptReply>
         @Override
         public long waitForEpoch()
         {
-            return txnId.epoch;
+            return txnId.epoch();
         }
     }
 }
diff --git a/accord-core/src/main/java/accord/messages/Apply.java b/accord-core/src/main/java/accord/messages/Apply.java
index 8554af2..c692d8d 100644
--- a/accord-core/src/main/java/accord/messages/Apply.java
+++ b/accord-core/src/main/java/accord/messages/Apply.java
@@ -73,7 +73,7 @@ public class Apply extends TxnRequest<ApplyReply>
     public void process()
     {
         // note, we do not also commit here if txnId.epoch != executeAt.epoch, as the scope() for a commit would be different
-        node.mapReduceConsumeLocal(this, txnId.epoch, untilEpoch, this);
+        node.mapReduceConsumeLocal(this, txnId.epoch(), untilEpoch, this);
     }
 
     public ApplyReply apply(SafeCommandStore safeStore)
@@ -101,8 +101,8 @@ public class Apply extends TxnRequest<ApplyReply>
     {
         if (reply == ApplyReply.Applied)
         {
-            node.ifLocal(empty(), scope.homeKey(), txnId.epoch, instance -> {
-                node.withEpoch(executeAt.epoch, () -> instance.progressLog().durableLocal(txnId));
+            node.ifLocal(empty(), scope.homeKey(), txnId.epoch(), instance -> {
+                node.withEpoch(executeAt.epoch(), () -> instance.progressLog().durableLocal(txnId));
             }).addCallback(node.agent());
         }
         node.reply(replyTo, replyContext, reply);
diff --git a/accord-core/src/main/java/accord/messages/BeginInvalidation.java b/accord-core/src/main/java/accord/messages/BeginInvalidation.java
index d4face3..57f48f7 100644
--- a/accord-core/src/main/java/accord/messages/BeginInvalidation.java
+++ b/accord-core/src/main/java/accord/messages/BeginInvalidation.java
@@ -33,9 +33,10 @@ public class BeginInvalidation extends AbstractEpochRequest<BeginInvalidation.In
         this.ballot = ballot;
     }
 
+    @Override
     public void process()
     {
-        node.mapReduceConsumeLocal(this, someUnseekables, txnId.epoch, txnId.epoch, this);
+        node.mapReduceConsumeLocal(this, someUnseekables, txnId.epoch(), txnId.epoch(), this);
     }
 
     @Override
@@ -79,7 +80,7 @@ public class BeginInvalidation extends AbstractEpochRequest<BeginInvalidation.In
     @Override
     public long waitForEpoch()
     {
-        return txnId.epoch;
+        return txnId.epoch();
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/messages/BeginRecovery.java b/accord-core/src/main/java/accord/messages/BeginRecovery.java
index db8958c..dad40c5 100644
--- a/accord-core/src/main/java/accord/messages/BeginRecovery.java
+++ b/accord-core/src/main/java/accord/messages/BeginRecovery.java
@@ -79,7 +79,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
     @Override
     protected void process()
     {
-        node.mapReduceConsumeLocal(this, txnId.epoch, txnId.epoch, this);
+        node.mapReduceConsumeLocal(this, txnId.epoch(), txnId.epoch(), this);
     }
 
     @Override
@@ -105,7 +105,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
         PartialDeps deps = command.partialDeps();
         if (!command.known().deps.isProposalKnown())
         {
-            deps = calculatePartialDeps(safeStore, txnId, partialTxn.keys(), partialTxn.kind(), txnId, safeStore.ranges().at(txnId.epoch));
+            deps = calculatePartialDeps(safeStore, txnId, partialTxn.keys(), partialTxn.kind(), txnId, safeStore.ranges().at(txnId.epoch()));
         }
 
         boolean rejectsFastPath;
@@ -118,7 +118,7 @@ public class BeginRecovery extends TxnRequest<BeginRecovery.RecoverReply>
         }
         else
         {
-            Ranges ranges = safeStore.ranges().at(txnId.epoch);
+            Ranges ranges = safeStore.ranges().at(txnId.epoch());
             rejectsFastPath = acceptedStartedAfterWithoutWitnessing(safeStore, txnId, ranges, partialTxn.keys()).anyMatch(ignore -> true);
             if (!rejectsFastPath)
                 rejectsFastPath = committedExecutesAfterWithoutWitnessing(safeStore, txnId, ranges, partialTxn.keys()).anyMatch(ignore -> true);
diff --git a/accord-core/src/main/java/accord/messages/Commit.java b/accord-core/src/main/java/accord/messages/Commit.java
index 1d9f47a..55c1c88 100644
--- a/accord-core/src/main/java/accord/messages/Commit.java
+++ b/accord-core/src/main/java/accord/messages/Commit.java
@@ -73,7 +73,7 @@ public class Commit extends TxnRequest<ReadNack>
             if (isHome)
                 sendRoute = route;
         }
-        else if (executeAt.epoch != txnId.epoch)
+        else if (executeAt.epoch() != txnId.epoch())
         {
             Ranges coordinateRanges = coordinateTopology.rangesForNode(to);
             Ranges executeRanges = topologies.computeRangesForNode(to);
@@ -104,11 +104,11 @@ public class Commit extends TxnRequest<ReadNack>
     public static void commitMinimalAndRead(Node node, Topologies executeTopologies, TxnId txnId, Txn txn, FullRoute<?> route, Seekables<?, ?> readScope, Timestamp executeAt, Deps deps, Set<Id> readSet, Callback<ReadReply> callback)
     {
         Topologies allTopologies = executeTopologies;
-        if (txnId.epoch != executeAt.epoch)
-            allTopologies = node.topology().preciseEpochs(route, txnId.epoch, executeAt.epoch);
+        if (txnId.epoch() != executeAt.epoch())
+            allTopologies = node.topology().preciseEpochs(route, txnId.epoch(), executeAt.epoch());
 
-        Topology executeTopology = executeTopologies.forEpoch(executeAt.epoch);
-        Topology coordinateTopology = allTopologies.forEpoch(txnId.epoch);
+        Topology executeTopology = executeTopologies.forEpoch(executeAt.epoch());
+        Topology coordinateTopology = allTopologies.forEpoch(txnId.epoch());
         for (Node.Id to : executeTopology.nodes())
         {
             boolean read = readSet.contains(to);
@@ -140,7 +140,7 @@ public class Commit extends TxnRequest<ReadNack>
 
     public void process()
     {
-        node.mapReduceConsumeLocal(this, txnId.epoch, executeAt.epoch, this);
+        node.mapReduceConsumeLocal(this, txnId.epoch(), executeAt.epoch(), this);
     }
 
     // TODO (expected, efficiency, clarity): do not guard with synchronized; let mapReduceLocal decide how to enforce mutual exclusivity
@@ -207,15 +207,15 @@ public class Commit extends TxnRequest<ReadNack>
 
         public static void commitInvalidate(Node node, TxnId txnId, Unseekables<?, ?> inform, Timestamp until)
         {
-            commitInvalidate(node, txnId, inform, until.epoch);
+            commitInvalidate(node, txnId, inform, until.epoch());
         }
 
         public static void commitInvalidate(Node node, TxnId txnId, Unseekables<?, ?> inform, long untilEpoch)
         {
             // TODO (expected, safety): this kind of check needs to be inserted in all equivalent methods
-            Invariants.checkState(untilEpoch >= txnId.epoch);
+            Invariants.checkState(untilEpoch >= txnId.epoch());
             Invariants.checkState(node.topology().hasEpoch(untilEpoch));
-            Topologies commitTo = node.topology().preciseEpochs(inform, txnId.epoch, untilEpoch);
+            Topologies commitTo = node.topology().preciseEpochs(inform, txnId.epoch(), untilEpoch);
             commitInvalidate(node, commitTo, txnId, inform);
         }
 
@@ -270,7 +270,7 @@ public class Commit extends TxnRequest<ReadNack>
 
         public void process(Node node, Id from, ReplyContext replyContext)
         {
-            node.forEachLocal(this, scope, txnId.epoch, invalidateUntilEpoch,
+            node.forEachLocal(this, scope, txnId.epoch(), invalidateUntilEpoch,
                             safeStore -> safeStore.command(txnId).commitInvalidate(safeStore))
                     .addCallback(node.agent());
         }
diff --git a/accord-core/src/main/java/accord/messages/GetDeps.java b/accord-core/src/main/java/accord/messages/GetDeps.java
index b2f296d..de4d518 100644
--- a/accord-core/src/main/java/accord/messages/GetDeps.java
+++ b/accord-core/src/main/java/accord/messages/GetDeps.java
@@ -44,13 +44,13 @@ public class GetDeps extends TxnRequest.WithUnsynced<PartialDeps>
 
     public void process()
     {
-        node.mapReduceConsumeLocal(this, minEpoch, executeAt.epoch, this);
+        node.mapReduceConsumeLocal(this, minEpoch, executeAt.epoch(), this);
     }
 
     @Override
     public PartialDeps apply(SafeCommandStore instance)
     {
-        Ranges ranges = instance.ranges().between(minEpoch, executeAt.epoch);
+        Ranges ranges = instance.ranges().between(minEpoch, executeAt.epoch());
         return calculatePartialDeps(instance, txnId, keys, kind, executeAt, ranges);
     }
 
diff --git a/accord-core/src/main/java/accord/messages/InformDurable.java b/accord-core/src/main/java/accord/messages/InformDurable.java
index da6123a..5586c22 100644
--- a/accord-core/src/main/java/accord/messages/InformDurable.java
+++ b/accord-core/src/main/java/accord/messages/InformDurable.java
@@ -55,7 +55,7 @@ public class InformDurable extends TxnRequest<Reply> implements PreLoadContext
             // TODO (required, consider): We might not replicate either txnId.epoch OR executeAt.epoch, but some inbetween.
             //                            Do we need to receive this message in that case? If so, we need to account for this when selecting a progress key
             at = executeAt;
-            progressKey = node.selectProgressKey(executeAt.epoch, scope, scope.homeKey());
+            progressKey = node.selectProgressKey(executeAt.epoch(), scope, scope.homeKey());
             shard = Adhoc;
         }
         else
@@ -64,7 +64,7 @@ public class InformDurable extends TxnRequest<Reply> implements PreLoadContext
         }
 
         // TODO (expected, efficiency): do not load from disk to perform this update
-        node.mapReduceConsumeLocal(contextFor(txnId), progressKey, at.epoch, this);
+        node.mapReduceConsumeLocal(contextFor(txnId), progressKey, at.epoch(), this);
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/messages/InformHomeDurable.java b/accord-core/src/main/java/accord/messages/InformHomeDurable.java
index a263cc7..1f91153 100644
--- a/accord-core/src/main/java/accord/messages/InformHomeDurable.java
+++ b/accord-core/src/main/java/accord/messages/InformHomeDurable.java
@@ -32,7 +32,7 @@ public class InformHomeDurable implements Request
     public void process(Node node, Id replyToNode, ReplyContext replyContext)
     {
         // TODO (expected, efficiency): do not load txnId first
-        node.ifLocal(contextFor(txnId), homeKey, txnId.epoch, safeStore -> {
+        node.ifLocal(contextFor(txnId), homeKey, txnId.epoch(), safeStore -> {
             Command command = safeStore.command(txnId);
             command.setDurability(safeStore, durability, homeKey, executeAt);
             safeStore.progressLog().durable(command, persistedOn);
diff --git a/accord-core/src/main/java/accord/messages/InformOfTxnId.java b/accord-core/src/main/java/accord/messages/InformOfTxnId.java
index a11b0db..946dbda 100644
--- a/accord-core/src/main/java/accord/messages/InformOfTxnId.java
+++ b/accord-core/src/main/java/accord/messages/InformOfTxnId.java
@@ -2,8 +2,6 @@ package accord.messages;
 
 import accord.api.RoutingKey;
 import accord.local.*;
-import accord.primitives.Keys;
-import accord.primitives.Seekables;
 import accord.primitives.TxnId;
 
 import java.util.Collections;
@@ -22,10 +20,11 @@ public class InformOfTxnId extends AbstractEpochRequest<Reply> implements Reques
         this.homeKey = homeKey;
     }
 
+    @Override
     public void process()
     {
         // TODO (expected, efficiency): do not first load txnId
-        node.mapReduceConsumeLocal(this, homeKey, txnId.epoch, this);
+        node.mapReduceConsumeLocal(this, homeKey, txnId.epoch(), this);
     }
 
     @Override
@@ -64,7 +63,7 @@ public class InformOfTxnId extends AbstractEpochRequest<Reply> implements Reques
     @Override
     public long waitForEpoch()
     {
-        return txnId.epoch;
+        return txnId.epoch();
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/messages/PreAccept.java b/accord-core/src/main/java/accord/messages/PreAccept.java
index 576797e..310c0a0 100644
--- a/accord-core/src/main/java/accord/messages/PreAccept.java
+++ b/accord-core/src/main/java/accord/messages/PreAccept.java
@@ -99,7 +99,7 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
             default:
             case Success:
             case Redundant:
-                return new PreAcceptOk(txnId, command.executeAt(), calculatePartialDeps(safeStore, txnId, partialTxn.keys(), partialTxn.kind(), txnId, safeStore.ranges().between(minEpoch, txnId.epoch)));
+                return new PreAcceptOk(txnId, command.executeAt(), calculatePartialDeps(safeStore, txnId, partialTxn.keys(), partialTxn.kind(), txnId, safeStore.ranges().between(minEpoch, txnId.epoch())));
 
             case RejectedBallot:
                 return PreAcceptNack.INSTANCE;
diff --git a/accord-core/src/main/java/accord/messages/ReadData.java b/accord-core/src/main/java/accord/messages/ReadData.java
index 9471c8d..d204de2 100644
--- a/accord-core/src/main/java/accord/messages/ReadData.java
+++ b/accord-core/src/main/java/accord/messages/ReadData.java
@@ -65,7 +65,7 @@ public class ReadData extends AbstractEpochRequest<ReadData.ReadNack> implements
     public ReadData(Node.Id to, Topologies topologies, TxnId txnId, Seekables<?, ?> readScope, Timestamp executeAt)
     {
         super(txnId);
-        this.executeAtEpoch = executeAt.epoch;
+        this.executeAtEpoch = executeAt.epoch();
         int startIndex = latestRelevantEpochIndex(to, topologies, readScope);
         this.readScope = computeScope(to, topologies, (Seekables)readScope, startIndex, Seekables::slice, Seekables::union);
         this.waitForEpoch = computeWaitForEpoch(to, topologies, startIndex);
diff --git a/accord-core/src/main/java/accord/messages/TxnRequest.java b/accord-core/src/main/java/accord/messages/TxnRequest.java
index b80968d..58bc8d4 100644
--- a/accord-core/src/main/java/accord/messages/TxnRequest.java
+++ b/accord-core/src/main/java/accord/messages/TxnRequest.java
@@ -56,15 +56,15 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
             this.minEpoch = topologies.oldestEpoch();
             this.doNotComputeProgressKey = doNotComputeProgressKey(topologies, startIndex, txnId, waitForEpoch());
 
-            Ranges ranges = topologies.forEpoch(txnId.epoch).rangesForNode(to);
+            Ranges ranges = topologies.forEpoch(txnId.epoch()).rangesForNode(to);
             if (doNotComputeProgressKey)
             {
                 Invariants.checkState(!route.intersects(ranges)); // confirm dest is not a replica on txnId.epoch
             }
             else if (Invariants.isParanoid())
             {
-                // check that the destination's newer topology does not yield different ranges
-                long progressEpoch = Math.min(waitForEpoch(), txnId.epoch);
+                boolean intersects = route.intersects(ranges);
+                long progressEpoch = Math.min(waitForEpoch(), txnId.epoch());
                 Ranges computesRangesOn = topologies.forEpoch(progressEpoch).rangesForNode(to);
                 if (computesRangesOn == null)
                     Invariants.checkState(!route.intersects(ranges));
@@ -147,7 +147,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
     RoutingKey progressKey(Node node)
     {
         // if waitForEpoch < txnId.epoch, then this replica's ownership is unchanged
-        long progressEpoch = min(waitForEpoch(), txnId.epoch);
+        long progressEpoch = min(waitForEpoch(), txnId.epoch());
         return node.trySelectProgressKey(progressEpoch, scope, scope.homeKey());
     }
 
@@ -276,7 +276,7 @@ public abstract class TxnRequest<R> implements Request, PreLoadContext, MapReduc
         // (as it might be done so with stale ring information)
 
         // TODO (low priority, clarity): this would be better defined as "hasProgressKey"
-        return waitForEpoch < txnId.epoch && startIndex > 0
-                && topologies.get(startIndex).epoch() < txnId.epoch;
+        return waitForEpoch < txnId.epoch() && startIndex > 0
+                && topologies.get(startIndex).epoch() < txnId.epoch();
     }
 }
diff --git a/accord-core/src/main/java/accord/messages/WaitOnCommit.java b/accord-core/src/main/java/accord/messages/WaitOnCommit.java
index 3a77406..490bc65 100644
--- a/accord-core/src/main/java/accord/messages/WaitOnCommit.java
+++ b/accord-core/src/main/java/accord/messages/WaitOnCommit.java
@@ -70,7 +70,7 @@ public class WaitOnCommit implements Request, MapReduceConsume<SafeCommandStore,
         this.node = node;
         this.replyTo = replyToNode;
         this.replyContext = replyContext;
-        node.mapReduceConsumeLocal(this, scope, txnId.epoch, txnId.epoch, this);
+        node.mapReduceConsumeLocal(this, scope, txnId.epoch(), txnId.epoch(), this);
     }
 
     @Override
@@ -189,6 +189,6 @@ public class WaitOnCommit implements Request, MapReduceConsume<SafeCommandStore,
     @Override
     public long waitForEpoch()
     {
-        return txnId.epoch;
+        return txnId.epoch();
     }
 }
diff --git a/accord-core/src/main/java/accord/primitives/AbstractKeys.java b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
index b777696..9b11cc4 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractKeys.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractKeys.java
@@ -13,7 +13,7 @@ import accord.api.RoutingKey;
 import accord.utils.*;
 import net.nicoulaj.compilecommand.annotations.Inline;
 
-import static accord.primitives.Routable.Kind.Key;
+import static accord.primitives.Routable.Domain.Key;
 
 @SuppressWarnings("rawtypes")
 // TODO (desired, efficiency): check that foldl call-sites are inlined and optimised by HotSpot
@@ -46,7 +46,7 @@ public abstract class AbstractKeys<K extends RoutableKey, KS extends Routables<K
     }
 
     @Override
-    public final Unseekable.Kind kindOfContents()
+    public final Routable.Domain kindOfContents()
     {
         return Key;
     }
diff --git a/accord-core/src/main/java/accord/primitives/AbstractRanges.java b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
index a87adce..9d5a4fa 100644
--- a/accord-core/src/main/java/accord/primitives/AbstractRanges.java
+++ b/accord-core/src/main/java/accord/primitives/AbstractRanges.java
@@ -96,9 +96,9 @@ public abstract class AbstractRanges<RS extends Routables<Range, ?>> implements
     }
 
     @Override
-    public final Unseekable.Kind kindOfContents()
+    public final Routable.Domain kindOfContents()
     {
-        return Unseekable.Kind.Range;
+        return Routable.Domain.Range;
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/primitives/Ballot.java b/accord-core/src/main/java/accord/primitives/Ballot.java
index 50d5a87..7e66668 100644
--- a/accord-core/src/main/java/accord/primitives/Ballot.java
+++ b/accord-core/src/main/java/accord/primitives/Ballot.java
@@ -22,6 +22,21 @@ import accord.local.Node.Id;
 
 public class Ballot extends Timestamp
 {
+    public static Ballot fromBits(long msb, long lsb, Id node)
+    {
+        return new Ballot(msb, lsb, node);
+    }
+
+    public static Ballot fromValues(long epoch, long hlc, Id node)
+    {
+        return fromValues(epoch, hlc, 0, node);
+    }
+
+    public static Ballot fromValues(long epoch, long hlc, int flags, Id node)
+    {
+        return new Ballot(epoch, hlc, flags, node);
+    }
+
     public static final Ballot ZERO = new Ballot(Timestamp.NONE);
 
     public Ballot(Timestamp from)
@@ -29,8 +44,18 @@ public class Ballot extends Timestamp
         super(from);
     }
 
-    public Ballot(long epoch, long real, int logical, Id node)
+    Ballot(long epoch, long hlc, int flags, Id node)
+    {
+        super(epoch, hlc, flags, node);
+    }
+
+    Ballot(long msb, long lsb, Id node)
+    {
+        super(msb, lsb, node);
+    }
+
+    public Ballot merge(Timestamp that)
     {
-        super(epoch, real, logical, node);
+        return merge(this, that, Ballot::fromBits);
     }
 }
diff --git a/accord-core/src/main/java/accord/primitives/Range.java b/accord-core/src/main/java/accord/primitives/Range.java
index 1a52d32..463a910 100644
--- a/accord-core/src/main/java/accord/primitives/Range.java
+++ b/accord-core/src/main/java/accord/primitives/Range.java
@@ -178,7 +178,7 @@ public abstract class Range implements Comparable<RoutableKey>, Unseekable, Seek
         return end;
     }
 
-    public final Kind kind() { return Kind.Range; }
+    public final Domain domain() { return Domain.Range; }
 
     public abstract boolean startInclusive();
     public abstract boolean endInclusive();
diff --git a/accord-core/src/main/java/accord/primitives/Routable.java b/accord-core/src/main/java/accord/primitives/Routable.java
index 575f1e8..a94132b 100644
--- a/accord-core/src/main/java/accord/primitives/Routable.java
+++ b/accord-core/src/main/java/accord/primitives/Routable.java
@@ -7,8 +7,28 @@ import accord.api.RoutingKey;
  */
 public interface Routable
 {
-    enum Kind { Key, Range }
-    Kind kind();
+    enum Domain
+    {
+        Key, Range;
+        private static final Domain[] VALUES = Domain.values();
+
+        public boolean isKey()
+        {
+            return this == Key;
+        }
+
+        public boolean isRange()
+        {
+            return this == Range;
+        }
+
+        public static Routable.Domain ofOrdinal(int ordinal)
+        {
+            return VALUES[ordinal];
+        }
+    }
+
+    Domain domain();
     Unseekable toUnseekable();
     RoutingKey someIntersectingRoutingKey();
 }
diff --git a/accord-core/src/main/java/accord/primitives/RoutableKey.java b/accord-core/src/main/java/accord/primitives/RoutableKey.java
index 4e90fe0..f8de9d6 100644
--- a/accord-core/src/main/java/accord/primitives/RoutableKey.java
+++ b/accord-core/src/main/java/accord/primitives/RoutableKey.java
@@ -46,7 +46,7 @@ public interface RoutableKey extends Routable, Comparable<RoutableKey>
      */
     int compareTo(@Nonnull RoutableKey that);
 
-    default Kind kind() { return Kind.Key; }
+    default Domain domain() { return Domain.Key; }
 
     RoutingKey toUnseekable();
 
diff --git a/accord-core/src/main/java/accord/primitives/Routables.java b/accord-core/src/main/java/accord/primitives/Routables.java
index 94d4baa..88eb4f5 100644
--- a/accord-core/src/main/java/accord/primitives/Routables.java
+++ b/accord-core/src/main/java/accord/primitives/Routables.java
@@ -57,7 +57,7 @@ public interface Routables<K extends Routable, U extends Routables<K, ?>> extend
      */
     int findNext(int thisIndex, K find, SortedArrays.Search search);
 
-    Routable.Kind kindOfContents();
+    Routable.Domain kindOfContents();
 
     @Inline
     static <Input extends Routable, T> T foldl(Routables<Input, ?> inputs, AbstractRanges<?> matching, IndexedFold<? super Input, T> fold, T initialValue)
diff --git a/accord-core/src/main/java/accord/primitives/Timestamp.java b/accord-core/src/main/java/accord/primitives/Timestamp.java
index 80c2660..5ac5e15 100644
--- a/accord-core/src/main/java/accord/primitives/Timestamp.java
+++ b/accord-core/src/main/java/accord/primitives/Timestamp.java
@@ -19,49 +19,124 @@
 package accord.primitives;
 
 import accord.local.Node.Id;
+import accord.utils.Invariants;
+
+import javax.annotation.Nonnull;
 
 public class Timestamp implements Comparable<Timestamp>
 {
+    public static Timestamp fromBits(long msb, long lsb, Id node)
+    {
+        return new Timestamp(msb, lsb, node);
+    }
+
+    public static Timestamp fromValues(long epoch, long hlc, Id node)
+    {
+        return new Timestamp(epoch, hlc, 0, node);
+    }
+
+    public static Timestamp fromValues(long epoch, long hlc, int flags, Id node)
+    {
+        return new Timestamp(epoch, hlc, flags, node);
+    }
+
+    public static Timestamp maxForEpoch(long epoch)
+    {
+        return new Timestamp(epochMsb(epoch) | 0xffff, Long.MAX_VALUE, Id.MAX);
+    }
+
+    public static Timestamp minForEpoch(long epoch)
+    {
+        return new Timestamp(epochMsb(epoch), 0, Id.NONE);
+    }
+
+    private static final long MAX_EPOCH = (1L << 48) - 1;
+    private static final long HLC_INCR = 1L << 16;
+    private static final long MAX_FLAGS = HLC_INCR - 1;
     public static final Timestamp NONE = new Timestamp(0, 0, 0, Id.NONE);
 
-    public final long epoch;
-    public final long real;
-    public final int logical;
+    public final long msb;
+    public final long lsb;
     public final Id node;
 
-    public Timestamp(long epoch, long real, int logical, Id node)
+    Timestamp(long epoch, long hlc, int flags, Id node)
+    {
+        Invariants.checkArgument(epoch <= MAX_EPOCH);
+        Invariants.checkArgument(flags <= MAX_FLAGS);
+        this.msb = epochMsb(epoch) | hlcMsb(hlc);
+        this.lsb = hlcLsb(hlc) | flags;
+        this.node = node;
+    }
+
+    Timestamp(long msb, long lsb, Id node)
     {
-        this.epoch = epoch;
-        this.real = real;
-        this.logical = logical;
+        this.msb = msb;
+        this.lsb = lsb;
         this.node = node;
     }
 
     public Timestamp(Timestamp copy)
     {
-        this.epoch = copy.epoch;
-        this.real = copy.real;
-        this.logical = copy.logical;
+        this.msb = copy.msb;
+        this.lsb = copy.lsb;
+        this.node = copy.node;
+    }
+
+    Timestamp(Timestamp copy, int flags)
+    {
+        Invariants.checkArgument(flags <= MAX_FLAGS);
+        this.msb = copy.msb;
+        this.lsb = notFlags(copy.lsb) | flags;
         this.node = copy.node;
     }
 
-    public Timestamp withMinEpoch(long minEpoch)
+    public long epoch()
     {
-        return minEpoch <= epoch ? this : new Timestamp(minEpoch, real, logical, node);
+        return epoch(msb);
+    }
+
+    /**
+     * A hybrid logical clock with implementation-defined resolution
+     */
+    public long hlc()
+    {
+        return highHlc(msb) | lowHlc(lsb);
+    }
+
+    public int flags()
+    {
+        return flags(lsb);
+    }
+
+    public Timestamp withEpochAtLeast(long minEpoch)
+    {
+        return minEpoch <= epoch() ? this : new Timestamp(minEpoch, hlc(), flags(), node);
     }
 
     public Timestamp logicalNext(Id node)
     {
-        return new Timestamp(epoch, real, logical + 1, node);
+        long lsb = this.lsb + HLC_INCR;
+        long msb = this.msb;
+        if (lowHlc(lsb) == 0)
+            ++msb; // overflow of lsb
+        return new Timestamp(msb, lsb, node);
     }
 
     @Override
-    public int compareTo(Timestamp that)
+    public int compareTo(@Nonnull Timestamp that)
+    {
+        if (this == that) return 0;
+        int c = Long.compareUnsigned(this.msb, that.msb);
+        if (c == 0) c = Long.compare(lowHlc(this.lsb), lowHlc(that.lsb));
+        if (c == 0) c = this.node.compareTo(that.node);
+        return c;
+    }
+
+    public int compareToStrict(@Nonnull Timestamp that)
     {
         if (this == that) return 0;
-        int c = Long.compare(this.epoch, that.epoch);
-        if (c == 0) c = Long.compare(this.real, that.real);
-        if (c == 0) c = Integer.compare(this.logical, that.logical);
+        int c = Long.compareUnsigned(this.msb, that.msb);
+        if (c == 0) c = Long.compareUnsigned(this.lsb, that.lsb);
         if (c == 0) c = this.node.compareTo(that.node);
         return c;
     }
@@ -69,12 +144,20 @@ public class Timestamp implements Comparable<Timestamp>
     @Override
     public int hashCode()
     {
-        return (int) (((((epoch * 31) + real) * 31) + node.hashCode()) * 31 + logical);
+        return (int) (((msb * 31) + lowHlc(lsb)) * 31) + node.hashCode();
     }
 
     public boolean equals(Timestamp that)
     {
-        return this.epoch == that.epoch && this.real == that.real && this.logical == that.logical && this.node.equals(that.node);
+        return this.msb == that.msb && lowHlc(this.lsb) == lowHlc(that.lsb) && this.node.equals(that.node);
+    }
+
+    /**
+     * Include flag bits in identity
+     */
+    public boolean equalsStrict(Timestamp that)
+    {
+        return this.msb == that.msb && this.lsb == that.lsb && this.node.equals(that.node);
     }
 
     @Override
@@ -98,10 +181,67 @@ public class Timestamp implements Comparable<Timestamp>
         return a.compareTo(b) <= 0 ? a : b;
     }
 
+    private static long epoch(long msb)
+    {
+        return msb >>> 16;
+    }
+
+    private static long epochMsb(long epoch)
+    {
+        return epoch << 16;
+    }
+
+    private static long hlcMsb(long hlc)
+    {
+        return hlc >>> 48;
+    }
+
+    private static long hlcLsb(long hlc)
+    {
+        return hlc << 16;
+    }
+
+    private static long highHlc(long msb)
+    {
+        return msb << 48;
+    }
+
+    private static long lowHlc(long lsb)
+    {
+        return lsb >>> 16;
+    }
+
+    private static int flags(long lsb)
+    {
+        return (int) (lsb & MAX_FLAGS);
+    }
+
+    private static long notFlags(long lsb)
+    {
+        return lsb & ~MAX_FLAGS;
+    }
+
+    public Timestamp merge(Timestamp that)
+    {
+        return merge(this, that, Timestamp::fromBits);
+    }
+
+    interface Constructor<T>
+    {
+        T construct(long msb, long lsb, Id node);
+    }
+
+    static <T extends Timestamp> T merge(Timestamp a, Timestamp b, Constructor<T> constructor)
+    {
+        Invariants.checkArgument(a.msb == b.msb);
+        Invariants.checkArgument(lowHlc(a.lsb) == lowHlc(b.lsb));
+        Invariants.checkArgument(a.node.equals(b.node));
+        return constructor.construct(a.msb, a.lsb | b.lsb, a.node);
+    }
+
     @Override
     public String toString()
     {
-        return "[" + epoch + ',' + real + ',' + logical + ',' + node + ']';
+        return "[" + epoch() + ',' + hlc() + ',' + flags() + ',' + node + ']';
     }
-
 }
diff --git a/accord-core/src/main/java/accord/primitives/Txn.java b/accord-core/src/main/java/accord/primitives/Txn.java
index 8703c04..2feead3 100644
--- a/accord-core/src/main/java/accord/primitives/Txn.java
+++ b/accord-core/src/main/java/accord/primitives/Txn.java
@@ -23,7 +23,6 @@ import java.util.List;
 import java.util.Objects;
 
 import accord.local.Command;
-import accord.local.CommandStore;
 
 import accord.api.*;
 import accord.local.SafeCommandStore;
@@ -37,11 +36,24 @@ public interface Txn
 {
     enum Kind
     {
-        READ, WRITE;
+        Read, Write;
+        // in future: BlindWrite, Interactive?
+
+        private static final Kind[] VALUES = Kind.values();
 
         public boolean isWrite()
         {
-            return this == WRITE;
+            return this == Write;
+        }
+
+        public boolean isRead()
+        {
+            return this == Read;
+        }
+
+        public static Kind ofOrdinal(int ordinal)
+        {
+            return VALUES[ordinal];
         }
     }
 
@@ -55,7 +67,7 @@ public interface Txn
 
         public InMemory(@Nonnull Seekables<?, ?> keys, @Nonnull Read read, @Nonnull Query query)
         {
-            this.kind = Kind.READ;
+            this.kind = Kind.Read;
             this.keys = keys;
             this.read = read;
             this.query = query;
@@ -64,7 +76,7 @@ public interface Txn
 
         public InMemory(@Nonnull Keys keys, @Nonnull Read read, @Nonnull Query query, @Nullable Update update)
         {
-            this.kind = Kind.WRITE;
+            this.kind = Kind.Write;
             this.keys = keys;
             this.read = read;
             this.update = update;
@@ -174,7 +186,7 @@ public interface Txn
 
     default Future<Data> read(SafeCommandStore safeStore, Command command)
     {
-        Ranges ranges = safeStore.ranges().at(command.executeAt().epoch);
+        Ranges ranges = safeStore.ranges().at(command.executeAt().epoch());
         List<Future<Data>> futures = read().keys().foldl(ranges, (key, accumulate, index) -> {
             Future<Data> result = read().read(key, kind(), safeStore, command.executeAt(), safeStore.dataStore());
             accumulate.add(result);
diff --git a/accord-core/src/main/java/accord/primitives/TxnId.java b/accord-core/src/main/java/accord/primitives/TxnId.java
index bff1e5c..e2b40bb 100644
--- a/accord-core/src/main/java/accord/primitives/TxnId.java
+++ b/accord-core/src/main/java/accord/primitives/TxnId.java
@@ -19,16 +19,106 @@
 package accord.primitives;
 
 import accord.local.Node.Id;
+import accord.primitives.Routable.Domain;
+import accord.primitives.Txn.Kind;
+
+import static accord.primitives.Txn.Kind.Read;
+import static accord.primitives.Txn.Kind.Write;
 
 public class TxnId extends Timestamp
 {
-    public TxnId(Timestamp timestamp)
+    public static TxnId fromBits(long msb, long lsb, Id node)
+    {
+        return new TxnId(msb, lsb, node);
+    }
+
+    public static TxnId fromValues(long epoch, long hlc, Id node)
+    {
+        return new TxnId(epoch, hlc, 0, node);
+    }
+
+    public static TxnId fromValues(long epoch, long hlc, int flags, Id node)
+    {
+        return new TxnId(epoch, hlc, flags, node);
+    }
+
+    public TxnId(Timestamp timestamp, Kind rw, Domain domain)
+    {
+        super(timestamp, flags(rw, domain));
+    }
+
+    public TxnId(long epoch, long hlc, Kind rw, Domain domain, Id node)
+    {
+        this(epoch, hlc, flags(rw, domain), node);
+    }
+
+    private TxnId(long epoch, long hlc, int flags, Id node)
+    {
+        super(epoch, hlc, flags, node);
+    }
+
+    private TxnId(long msb, long lsb, Id node)
+    {
+        super(msb, lsb, node);
+    }
+
+    public boolean isWrite()
+    {
+        return rwOrdinal(flags()) == Write.ordinal();
+    }
+
+    public boolean isRead()
+    {
+        return rwOrdinal(flags()) == Read.ordinal();
+    }
+
+    public Kind rw()
+    {
+        return rw(flags());
+    }
+
+    public Domain domain()
+    {
+        return domain(flags());
+    }
+
+    public TxnId merge(Timestamp that)
+    {
+        return merge(this, that, TxnId::fromBits);
+    }
+
+    private static int flags(Kind rw, Domain domain)
+    {
+        return flags(rw) | flags(domain);
+    }
+
+    private static int flags(Kind rw)
+    {
+        return rw.ordinal() << 1;
+    }
+
+    private static int flags(Domain domain)
+    {
+        return domain.ordinal();
+    }
+
+    private static Kind rw(int flags)
+    {
+        return Kind.ofOrdinal(rwOrdinal(flags));
+    }
+
+    private static Domain domain(int flags)
+    {
+        return Domain.ofOrdinal(domainOrdinal(flags));
+    }
+
+    private static int rwOrdinal(int flags)
     {
-        super(timestamp);
+        return (flags >> 1) & 1;
     }
 
-    public TxnId(long epoch, long real, int logical, Id node)
+    private static int domainOrdinal(int flags)
     {
-        super(epoch, real, logical, node);
+        return flags & 1;
     }
 }
diff --git a/accord-core/src/main/java/accord/primitives/Writes.java b/accord-core/src/main/java/accord/primitives/Writes.java
index f8d90b5..c3f7635 100644
--- a/accord-core/src/main/java/accord/primitives/Writes.java
+++ b/accord-core/src/main/java/accord/primitives/Writes.java
@@ -67,7 +67,7 @@ public class Writes
         if (write == null)
             return SUCCESS;
 
-        Ranges ranges = safeStore.ranges().since(executeAt.epoch);
+        Ranges ranges = safeStore.ranges().since(executeAt.epoch());
         if (ranges == null)
             return SUCCESS;
 
diff --git a/accord-core/src/main/java/accord/topology/TopologyManager.java b/accord-core/src/main/java/accord/topology/TopologyManager.java
index 38d6589..6665ef5 100644
--- a/accord-core/src/main/java/accord/topology/TopologyManager.java
+++ b/accord-core/src/main/java/accord/topology/TopologyManager.java
@@ -294,7 +294,7 @@ public class TopologyManager implements ConfigurationService.Listener
 
     public Topologies withUnsyncedEpochs(Unseekables<?, ?> select, Timestamp at)
     {
-        return withUnsyncedEpochs(select, at.epoch);
+        return withUnsyncedEpochs(select, at.epoch());
     }
 
     public Topologies withUnsyncedEpochs(Unseekables<?, ?> select, long epoch)
@@ -304,7 +304,7 @@ public class TopologyManager implements ConfigurationService.Listener
 
     public Topologies withUnsyncedEpochs(Unseekables<?, ?> select, Timestamp min, Timestamp max)
     {
-        return withUnsyncedEpochs(select, min.epoch, max.epoch);
+        return withUnsyncedEpochs(select, min.epoch(), max.epoch());
     }
 
     public Topologies withUnsyncedEpochs(Unseekables<?, ?> select, long minEpoch, long maxEpoch)
diff --git a/accord-core/src/test/java/accord/burn/TopologyUpdates.java b/accord-core/src/test/java/accord/burn/TopologyUpdates.java
index 61aa6b9..cc8645a 100644
--- a/accord-core/src/test/java/accord/burn/TopologyUpdates.java
+++ b/accord-core/src/test/java/accord/burn/TopologyUpdates.java
@@ -118,12 +118,12 @@ public class TopologyUpdates
                     break;
                 case PreApplied:
                 case Applied:
-                    node.withEpoch(Math.max(executeAt.epoch, toEpoch), () -> {
+                    node.withEpoch(Math.max(executeAt.epoch(), toEpoch), () -> {
                         FetchData.fetch(PreApplied.minKnown, node, txnId, route, executeAt, toEpoch, callback);
                     });
                     break;
                 case Invalidated:
-                    node.forEachLocal(contextFor(txnId), route, txnId.epoch, toEpoch, safeStore -> {
+                    node.forEachLocal(contextFor(txnId), route, txnId.epoch(), toEpoch, safeStore -> {
                         safeStore.command(txnId).commitInvalidate(safeStore);
                     });
             }
diff --git a/accord-core/src/test/java/accord/coordinate/CoordinateTest.java b/accord-core/src/test/java/accord/coordinate/CoordinateTest.java
index 3b94f26..8017b21 100644
--- a/accord-core/src/test/java/accord/coordinate/CoordinateTest.java
+++ b/accord-core/src/test/java/accord/coordinate/CoordinateTest.java
@@ -33,6 +33,8 @@ import static accord.Utils.id;
 import static accord.Utils.ids;
 import static accord.Utils.writeTxn;
 import static accord.impl.IntKey.keys;
+import static accord.primitives.Routable.Domain.Key;
+import static accord.primitives.Txn.Kind.Write;
 
 public class CoordinateTest
 {
@@ -44,7 +46,7 @@ public class CoordinateTest
             Node node = cluster.get(1);
             Assertions.assertNotNull(node);
 
-            TxnId txnId = node.nextTxnId();
+            TxnId txnId = node.nextTxnId(Write, Key);
             Keys keys = keys(10);
             Txn txn = writeTxn(keys);
             FullKeyRoute route = keys.toRoute(keys.get(0).toUnseekable());
@@ -71,8 +73,8 @@ public class CoordinateTest
 
     private TxnId coordinate(Node node, long clock, Keys keys) throws Throwable
     {
-        TxnId txnId = node.nextTxnId();
-        txnId = new TxnId(txnId.epoch, txnId.real + clock, 0, txnId.node);
+        TxnId txnId = node.nextTxnId(Write, Key);
+        txnId = new TxnId(txnId.epoch(), txnId.hlc() + clock, Write, Key, txnId.node);
         Txn txn = writeTxn(keys);
         Result result = Coordinate.coordinate(node, txnId, txn, node.computeRoute(txnId, txn.keys())).get();
         Assertions.assertEquals(MockStore.RESULT, result);
@@ -135,7 +137,7 @@ public class CoordinateTest
             Node node = cluster.get(1);
             Assertions.assertNotNull(node);
 
-            TxnId txnId = node.nextTxnId();
+            TxnId txnId = node.nextTxnId(Write, Key);
             Keys oneKey = keys(10);
             Keys twoKeys = keys(10, 20);
             Txn txn = new Txn.InMemory(oneKey, MockStore.read(oneKey), MockStore.QUERY, MockStore.update(twoKeys));
diff --git a/accord-core/src/test/java/accord/coordinate/TopologyChangeTest.java b/accord-core/src/test/java/accord/coordinate/TopologyChangeTest.java
index efba8ef..dbde032 100644
--- a/accord-core/src/test/java/accord/coordinate/TopologyChangeTest.java
+++ b/accord-core/src/test/java/accord/coordinate/TopologyChangeTest.java
@@ -18,14 +18,11 @@
 
 package accord.coordinate;
 
-import accord.impl.mock.EpochSync;
 import accord.impl.mock.MockCluster;
 import accord.impl.mock.MockConfigurationService;
-import accord.impl.mock.RecordingMessageSink;
 import accord.local.Command;
 import accord.local.Node;
 import accord.local.Status;
-import accord.messages.Accept;
 import accord.primitives.Range;
 import accord.topology.Topology;
 import accord.primitives.Keys;
@@ -35,25 +32,15 @@ import accord.utils.EpochFunction;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-import java.util.ArrayList;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 import static accord.Utils.*;
 import static accord.impl.IntKey.keys;
 import static accord.impl.IntKey.range;
 import static accord.local.PreLoadContext.empty;
+import static accord.primitives.Routable.Domain.Key;
+import static accord.primitives.Txn.Kind.Write;
 
 public class TopologyChangeTest
 {
-    private static TxnId coordinate(Node node, Keys keys) throws Throwable
-    {
-        TxnId txnId = node.nextTxnId();
-        Txn txn = writeTxn(keys);
-        node.coordinate(txnId, txn).get();
-        return txnId;
-    }
-
     @Test
     void disjointElectorate() throws Throwable
     {
@@ -72,7 +59,7 @@ public class TopologyChangeTest
                                               .build())
         {
             Node node1 = cluster.get(1);
-            TxnId txnId1 = node1.nextTxnId();
+            TxnId txnId1 = node1.nextTxnId(Write, Key);
             Txn txn1 = writeTxn(keys);
             node1.coordinate(txnId1, txn1).get();
             node1.commandStores().forEach(empty(), keys, 1, 1, commands -> {
@@ -83,7 +70,7 @@ public class TopologyChangeTest
             cluster.configServices(4, 5, 6).forEach(config -> config.reportTopology(topology2));
 
             Node node4 = cluster.get(4);
-            TxnId txnId2 = node4.nextTxnId();
+            TxnId txnId2 = node4.nextTxnId(Write, Key);
             Txn txn2 = writeTxn(keys);
             node4.coordinate(txnId2, txn2).get();
 
diff --git a/accord-core/src/test/java/accord/impl/TestAgent.java b/accord-core/src/test/java/accord/impl/TestAgent.java
index b417dbc..53ff4f6 100644
--- a/accord-core/src/test/java/accord/impl/TestAgent.java
+++ b/accord-core/src/test/java/accord/impl/TestAgent.java
@@ -56,6 +56,6 @@ public class TestAgent implements Agent
     @Override
     public boolean isExpired(TxnId initiated, long now)
     {
-        return TimeUnit.SECONDS.convert(now - initiated.real, TimeUnit.MICROSECONDS) >= 10;
+        return TimeUnit.SECONDS.convert(now - initiated.hlc(), TimeUnit.MICROSECONDS) >= 10;
     }
 }
diff --git a/accord-core/src/test/java/accord/impl/list/ListAgent.java b/accord-core/src/test/java/accord/impl/list/ListAgent.java
index 158c42b..2d6bb1f 100644
--- a/accord-core/src/test/java/accord/impl/list/ListAgent.java
+++ b/accord-core/src/test/java/accord/impl/list/ListAgent.java
@@ -70,6 +70,6 @@ public class ListAgent implements Agent
     @Override
     public boolean isExpired(TxnId initiated, long now)
     {
-        return now - initiated.real >= timeout;
+        return now - initiated.hlc() >= timeout;
     }
 }
diff --git a/accord-core/src/test/java/accord/impl/list/ListRequest.java b/accord-core/src/test/java/accord/impl/list/ListRequest.java
index fa8a29e..cdef9b0 100644
--- a/accord-core/src/test/java/accord/impl/list/ListRequest.java
+++ b/accord-core/src/test/java/accord/impl/list/ListRequest.java
@@ -34,7 +34,6 @@ import accord.messages.CheckStatus.IncludeInfo;
 import accord.messages.MessageType;
 import accord.messages.ReplyContext;
 import accord.primitives.RoutingKeys;
-import accord.primitives.RoutingKeys;
 import accord.primitives.Txn;
 import accord.messages.Request;
 import accord.primitives.TxnId;
@@ -51,7 +50,7 @@ public class ListRequest implements Request
         int count = 0;
         protected CheckOnResult(Node node, TxnId txnId, RoutingKey homeKey, BiConsumer<Outcome, Throwable> callback)
         {
-            super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch, IncludeInfo.All);
+            super(node, txnId, RoutingKeys.of(homeKey), txnId.epoch(), IncludeInfo.All);
             this.callback = callback;
         }
 
diff --git a/accord-core/src/test/java/accord/impl/mock/MockCluster.java b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
index 7680042..c6f70c2 100644
--- a/accord-core/src/test/java/accord/impl/mock/MockCluster.java
+++ b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
@@ -26,6 +26,8 @@ import accord.local.Node;
 import accord.local.Node.Id;
 import accord.local.ShardDistributor;
 import accord.primitives.Ranges;
+import accord.primitives.Routable;
+import accord.primitives.Txn;
 import accord.utils.EpochFunction;
 import accord.utils.ThreadPoolScheduler;
 import accord.primitives.TxnId;
@@ -45,6 +47,8 @@ import java.util.function.BiFunction;
 import java.util.function.LongSupplier;
 
 import static accord.Utils.*;
+import static accord.primitives.Routable.Domain.Key;
+import static accord.primitives.Txn.Kind.Write;
 
 public class MockCluster implements Network, AutoCloseable, Iterable<Node>
 {
@@ -382,7 +386,7 @@ public class MockCluster implements Network, AutoCloseable, Iterable<Node>
 
         public TxnId idForNode(long epoch, Id id)
         {
-            return new TxnId(epoch, now.get(), 0, id);
+            return new TxnId(epoch, now.get(), Write, Key, id);
         }
 
         public TxnId idForNode(long epoch, int id)
diff --git a/accord-core/src/test/java/accord/local/CommandTest.java b/accord-core/src/test/java/accord/local/CommandTest.java
index 1b57025..443d31c 100644
--- a/accord-core/src/test/java/accord/local/CommandTest.java
+++ b/accord-core/src/test/java/accord/local/CommandTest.java
@@ -47,6 +47,8 @@ import java.util.function.Consumer;
 import static accord.Utils.id;
 import static accord.Utils.writeTxn;
 import static accord.impl.InMemoryCommandStore.inMemory;
+import static accord.primitives.Routable.Domain.Key;
+import static accord.primitives.Txn.Kind.Write;
 
 public class CommandTest
 {
@@ -166,7 +168,7 @@ public class CommandTest
         CommandStoreSupport support = new CommandStoreSupport();
         Node node = createNode(ID1, support);
         CommandStore commands = node.unsafeByIndex(0);
-        TxnId txnId = node.nextTxnId();
+        TxnId txnId = node.nextTxnId(Write, Key);
         ((MockCluster.Clock)node.unsafeGetNowSupplier()).increment(10);
         Txn txn = writeTxn(Keys.of(KEY));
 
@@ -176,7 +178,7 @@ public class CommandTest
 
         setTopologyEpoch(support.local, 2);
         ((TestableConfigurationService)node.configService()).reportTopology(support.local.get().withEpoch(2));
-        Timestamp expectedTimestamp = new Timestamp(2, 110, 0, ID1);
+        Timestamp expectedTimestamp = Timestamp.fromValues(2, 110, ID1);
         commands.execute(null, (Consumer<? super SafeCommandStore>) store -> command.preaccept(store, txn.slice(FULL_RANGES, true), ROUTE, HOME_KEY)).syncUninterruptibly();
         Assertions.assertEquals(Status.PreAccepted, command.status());
         Assertions.assertEquals(expectedTimestamp, command.executeAt());
diff --git a/accord-core/src/test/java/accord/local/NodeTest.java b/accord-core/src/test/java/accord/local/NodeTest.java
index 4303b93..8d02d30 100644
--- a/accord-core/src/test/java/accord/local/NodeTest.java
+++ b/accord-core/src/test/java/accord/local/NodeTest.java
@@ -26,9 +26,9 @@ import org.junit.jupiter.api.Test;
 
 public class NodeTest
 {
-    private static Timestamp ts(long epoch, long real, int logical, int node)
+    private static Timestamp ts(long epoch, long hlc, int node)
     {
-        return new Timestamp(epoch, real, logical, new Node.Id(node));
+        return Timestamp.fromValues(epoch, hlc, new Node.Id(node));
     }
 
     @Test
@@ -43,13 +43,14 @@ public class NodeTest
             Timestamp timestamp1 = node.uniqueNow();
             Timestamp timestamp2 = node.uniqueNow();
 
+            clock.increment();
+            clock.increment();
             clock.increment();
             Timestamp timestamp3 = node.uniqueNow();
 
-            Assertions.assertEquals(ts(1, 101, 0, 1), timestamp1);
-            Assertions.assertEquals(ts(1, 101, 1, 1), timestamp2);
-
-            Assertions.assertEquals(ts(1, 102, 0, 1), timestamp3);
+            Assertions.assertEquals(ts(1, 101, 1), timestamp1);
+            Assertions.assertEquals(ts(1, 102, 1), timestamp2);
+            Assertions.assertEquals(ts(1, 104, 1), timestamp3);
         }
     }
 
@@ -64,11 +65,11 @@ public class NodeTest
 
             clock.increment();
             Timestamp timestamp1 = node.uniqueNow();
-            Assertions.assertEquals(ts(1, 101, 0, 1), timestamp1);
+            Assertions.assertEquals(ts(1, 101, 1), timestamp1);
 
             configService.reportTopology(node.topology().current().withEpoch(2));
             Timestamp timestamp2 = node.uniqueNow();
-            Assertions.assertEquals(ts(2, 101, 1, 1), timestamp2);
+            Assertions.assertEquals(ts(2, 102, 1), timestamp2);
         }
     }
 
@@ -82,19 +83,15 @@ public class NodeTest
 
             clock.increment();
             Timestamp timestamp1 = node.uniqueNow();
-            Assertions.assertEquals(ts(1, 101, 0, 1), timestamp1);
+            Assertions.assertEquals(ts(1, 101, 1), timestamp1);
 
-            // atLeast equal to most recent ts, logical should increment
-            Assertions.assertEquals(ts(1, 101, 1, 1),
+            // atLeast equal to most recent ts, should simply increment
+            Assertions.assertEquals(ts(1, 102, 1),
                                     node.uniqueNow(timestamp1));
 
-            // atLeast less than most recent ts
-            Assertions.assertEquals(ts(1, 101, 2, 1),
-                                    node.uniqueNow(ts(1, 99, 0, 1)));
-
             // atLeast greater than most recent ts
-            Assertions.assertEquals(ts(1, 110, 1, 1),
-                                    node.uniqueNow(ts(1, 110, 0, 2)));
+            Assertions.assertEquals(ts(1, 111, 1),
+                                    node.uniqueNow(ts(1, 110, 2)));
         }
     }
 }
diff --git a/accord-core/src/test/java/accord/messages/PreAcceptTest.java b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
index a8ae211..f1f1117 100644
--- a/accord-core/src/test/java/accord/messages/PreAcceptTest.java
+++ b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
@@ -40,11 +40,13 @@ import org.junit.jupiter.api.Test;
 import java.util.List;
 import java.util.Random;
 
-import static accord.Utils.id;
-import static accord.Utils.writeTxn;
+import static accord.Utils.*;
 import static accord.impl.InMemoryCommandStore.inMemory;
+import static accord.impl.IntKey.range;
 import static accord.impl.IntKey.routing;
 import static accord.impl.mock.MockCluster.configService;
+import static accord.primitives.Routable.Domain.Key;
+import static accord.primitives.Txn.Kind.Write;
 import static accord.utils.Utils.listOf;
 
 public class PreAcceptTest
@@ -79,7 +81,7 @@ public class PreAcceptTest
     private static PreAccept preAccept(TxnId txnId, Txn txn, RoutingKey homeKey)
     {
         FullRoute<?> route = txn.keys().toRoute(homeKey);
-        return PreAccept.SerializerSupport.create(txnId, route.slice(FULL_RANGE), txnId.epoch, txnId.epoch, false, txnId.epoch, txn.slice(FULL_RANGE, true), route);
+        return PreAccept.SerializerSupport.create(txnId, route.slice(FULL_RANGE), txnId.epoch(), txnId.epoch(), false, txnId.epoch(), txn.slice(FULL_RANGE, true), route);
     }
 
     @Test
@@ -159,15 +161,15 @@ public class PreAcceptTest
             messageSink.clearHistory();
             Raw key2 = IntKey.key(11);
             Keys keys = Keys.of(key1, key2);
-            TxnId txnId2 = new TxnId(1, 50, 0, ID3);
+            TxnId txnId2 = new TxnId(1, 50, Write, Key, ID3);
             PreAccept preAccept2 = preAccept(txnId2, writeTxn(keys), key2.toUnseekable());
             clock.increment(10);
             preAccept2.process(node, ID3, REPLY_CONTEXT);
 
             messageSink.assertHistorySizes(0, 1);
             Assertions.assertEquals(ID3, messageSink.responses.get(0).to);
-            PartialDeps expectedDeps = PartialDeps.NONE;
-            Assertions.assertEquals(new PreAccept.PreAcceptOk(txnId2, new TxnId(1, 110, 0, ID1), expectedDeps),
+            PartialDeps expectedDeps = Deps.NONE.slice(ranges(range(0, 12)));
+            Assertions.assertEquals(new PreAccept.PreAcceptOk(txnId2, Timestamp.fromValues(1, 110, ID1), expectedDeps),
                                     messageSink.responses.get(0).payload);
         }
         finally
@@ -191,7 +193,7 @@ public class PreAcceptTest
         try
         {
             Keys keys = Keys.of(key);
-            TxnId txnId = new TxnId(1, 110, 0, ID2);
+            TxnId txnId = new TxnId(1, 110, Write, Key, ID2);
             PreAccept preAccept = preAccept(txnId, writeTxn(keys), key.toUnseekable());
             preAccept.process(node, ID2, REPLY_CONTEXT);
 
@@ -235,7 +237,7 @@ public class PreAcceptTest
 
             messageSink.assertHistorySizes(0, 1);
             Assertions.assertEquals(ID2, messageSink.responses.get(0).to);
-            Assertions.assertEquals(new PreAccept.PreAcceptOk(txnId, new TxnId(2, 110, 0, ID1), PartialDeps.NONE),
+            Assertions.assertEquals(new PreAccept.PreAcceptOk(txnId, Timestamp.fromValues(2, 110, ID1), Deps.NONE.slice(ranges(range(0, 12)))),
                                     messageSink.responses.get(0).payload);
         }
         finally
diff --git a/accord-core/src/test/java/accord/txn/DepsTest.java b/accord-core/src/test/java/accord/txn/DepsTest.java
index 9bd8d52..9c480ad 100644
--- a/accord-core/src/test/java/accord/txn/DepsTest.java
+++ b/accord-core/src/test/java/accord/txn/DepsTest.java
@@ -65,23 +65,23 @@ public class DepsTest
     @Test
     public void testRandom()
     {
-        testOneRandom(seed(), 1000, 3, 50, 10, 4, 100, 10, 200, 1000);
-        testOneRandom(seed(), 1000, 3, 50, 10, 4, 10, 2, 200, 1000);
-        testOneRandom(seed(), 100, 3, 50, 10, 4, 10, 2, 200, 100);
+        testOneRandom(seed(), 1000, 3, 500, 4, 100, 10, 200, 1000);
+        testOneRandom(seed(), 1000, 3, 500, 4, 10, 2, 200, 1000);
+        testOneRandom(seed(), 100, 3, 500, 4, 10, 2, 200, 100);
     }
 
     @Test
     public void testMerge()
     {
-        testMerge(seed(), 100, 3, 50, 10, 4, 10, 5, 200, 100, 10);
-        testMerge(seed(), 1000, 3, 50, 10, 4, 100, 10, 200, 1000, 10);
+        testMerge(seed(), 100, 3, 500, 4, 10, 5, 200, 100, 10);
+        testMerge(seed(), 1000, 3, 500, 4, 100, 10, 200, 1000, 10);
     }
 
-    private static void testMerge(long seed, int uniqueTxnIdsRange, int epochRange, int realRange, int logicalRange, int nodeRange,
+    private static void testMerge(long seed, int uniqueTxnIdsRange, int epochRange, int hlcRange, int nodeRange,
                                  int uniqueKeysRange, int emptyKeysRange, int keyRange, int totalCountRange, int mergeCountRange)
     {
         Random random = random(seed);
-        Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, epochRange, realRange, logicalRange, nodeRange,
+        Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, epochRange, hlcRange, 0, nodeRange,
                                            uniqueKeysRange, emptyKeysRange, keyRange, totalCountRange);
         int count = 1 + random.nextInt(mergeCountRange);
         List<Deps> deps = new ArrayList<>(count);
@@ -93,14 +93,14 @@ public class DepsTest
     @Test
     public void testWith()
     {
-        testWith(seed(), 1000, 3, 50, 10, 4, 100, 10, 200, 1000, 10);
+        testWith(seed(), 1000, 3, 500, 4, 100, 10, 200, 1000, 10);
     }
 
-    private static void testWith(long seed, int uniqueTxnIdsRange, int epochRange, int realRange, int logicalRange, int nodeRange,
+    private static void testWith(long seed, int uniqueTxnIdsRange, int epochRange, int hlcRange, int nodeRange,
                                  int uniqueKeysRange, int emptyKeysRange, int keyRange, int totalCountRange, int mergeCountRange)
     {
         Random random = random(seed);
-        Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, epochRange, realRange, logicalRange, nodeRange,
+        Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, epochRange, hlcRange, 0, nodeRange,
                                            uniqueKeysRange, emptyKeysRange, keyRange, totalCountRange);
         Deps cur = supplier.get();
         int count = 1 + random.nextInt(mergeCountRange);
@@ -311,22 +311,21 @@ public class DepsTest
         static Deps generate(Gen.Random random)
         {
             int epochRange = 3;
-            int realRange = 50;
-            int logicalRange = 10;
+            int hlcRange = 500;
             double uniqueTxnIdsPercentage = 0.66D;
-            int uniqueTxnIds = random.nextPositive((int) ((realRange * logicalRange * epochRange) * uniqueTxnIdsPercentage));
+            int uniqueTxnIds = random.nextPositive((int) ((hlcRange * epochRange) * uniqueTxnIdsPercentage));
 
             int nodeRange = random.nextPositive(4);
             int uniqueKeys = random.nextInt(2, 200);
             int emptyKeys = random.nextInt(0, 10);
             int keyRange = random.nextInt(uniqueKeys + emptyKeys, 400);
             int totalCount = random.nextPositive(1000);
-            Deps deps = generate(random, uniqueTxnIds, epochRange, realRange, logicalRange, nodeRange, uniqueKeys, emptyKeys, keyRange, totalCount);
+            Deps deps = generate(random, uniqueTxnIds, epochRange, hlcRange, 0, nodeRange, uniqueKeys, emptyKeys, keyRange, totalCount);
             deps.testSimpleEquality();
             return deps;
         }
 
-        static Deps generate(Random random, int uniqueTxnIds, int epochRange, int realRange, int logicalRange, int nodeRange,
+        static Deps generate(Random random, int uniqueTxnIds, int epochRange, int hlcRange, int flagsRange, int nodeRange,
                              int uniqueKeys, int emptyKeys, int keyRange, int totalCount)
         {
             // populateKeys is a subset of keys
@@ -344,7 +343,7 @@ public class DepsTest
             List<TxnId> txnIds; {
                 TreeSet<TxnId> tmp = new TreeSet<>();
                 while (tmp.size() < uniqueTxnIds)
-                    tmp.add(new TxnId(random.nextInt(epochRange), random.nextInt(realRange), random.nextInt(logicalRange), new Id(random.nextInt(nodeRange))));
+                    tmp.add(TxnId.fromValues(random.nextInt(epochRange), random.nextInt(hlcRange), flagsRange == 0 ? 0 : random.nextInt(flagsRange), new Id(random.nextInt(nodeRange))));
                 txnIds = new ArrayList<>(tmp);
             }
 
@@ -469,17 +468,17 @@ public class DepsTest
         return Ranges.ofSortedAndDeoverlapped(ranges);
     }
 
-    private static void testOneRandom(long seed, int uniqueTxnIds, int epochRange, int realRange, int logicalRange, int nodeRange,
+    private static void testOneRandom(long seed, int uniqueTxnIds, int epochRange, int hlcRange, int nodeRange,
                                       int uniqueKeys, int emptyKeys, int keyRange, int totalCountRange)
     {
         Random random = random(seed);
         int totalCount = 1 + random.nextInt(totalCountRange - 1);
         testOneDeps(random,
-                    DepsTest.Deps.generate(random, uniqueTxnIds, epochRange, realRange, logicalRange, nodeRange, uniqueKeys, emptyKeys, keyRange, totalCount),
+                    DepsTest.Deps.generate(random, uniqueTxnIds, epochRange, hlcRange, 0, nodeRange, uniqueKeys, emptyKeys, keyRange, totalCount),
                     keyRange);
     }
 
-    private static Supplier<Deps> supplier(Random random, int uniqueTxnIdsRange, int epochRange, int realRange, int logicalRange, int nodeRange,
+    private static Supplier<Deps> supplier(Random random, int uniqueTxnIdsRange, int epochRange, int hlcRange, int flagRange, int nodeRange,
                                            int uniqueKeysRange, int emptyKeysRange, int keyRange, int totalCountRange)
     {
         return () -> {
@@ -491,7 +490,7 @@ public class DepsTest
             int emptyKeys = 1 + random.nextInt(emptyKeysRange - 1);
             int totalCount = random.nextInt(Math.min(totalCountRange, uniqueKeys * uniqueTxnIds));
             return DepsTest.Deps.generate(random, uniqueTxnIds,
-                                                  epochRange, realRange, logicalRange, nodeRange,
+                                                  epochRange, hlcRange, flagRange, nodeRange,
                                                   uniqueKeys, emptyKeys, keyRange, totalCount);
         };
     }
@@ -574,10 +573,10 @@ public class DepsTest
     public static void main(String[] args)
     {
         for (long seed = 0 ; seed < 10000 ; ++seed)
-            testMerge(seed, 100, 3, 50, 10, 4, 4, 2, 100, 10, 4);
+            testMerge(seed, 100, 3, 50, 4, 4, 2, 100, 10, 4);
 
         for (long seed = 0 ; seed < 10000 ; ++seed)
-            testMerge(seed, 1000, 3, 50, 10, 4, 20, 5, 200, 100, 4);
+            testMerge(seed, 1000, 3, 50, 4, 20, 5, 200, 100, 4);
     }
 
 }
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
index e374f01..98a8345 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
@@ -113,13 +113,13 @@ public class Json
                 in.nextNull();
                 return null;
             }
-            return readTimestamp(in, Timestamp::new);
+            return readTimestamp(in, Timestamp::fromBits);
         }
     };
 
     private interface TimestampFactory<T>
     {
-        T create(long epoch, long real, int logical, Id node);
+        T create(long msb, long lsb, Id node);
     }
 
     private static <T> T readTimestamp(JsonReader in, TimestampFactory<T> factory) throws IOException
@@ -130,12 +130,11 @@ public class Json
             return null;
         }
         in.beginArray();
-        long epoch = in.nextLong();
-        long real = in.nextLong();
-        int logical = in.nextInt();
+        long msb = in.nextLong();
+        long lsb = in.nextLong();
         Id node = ID_ADAPTER.read(in);
         in.endArray();
-        return factory.create(epoch, real, logical, node);
+        return factory.create(msb, lsb, node);
     }
 
     private static void writeTimestamp(JsonWriter out, Timestamp timestamp) throws IOException
@@ -146,9 +145,8 @@ public class Json
             return;
         }
         out.beginArray();
-        out.value(timestamp.epoch);
-        out.value(timestamp.real);
-        out.value(timestamp.logical);
+        out.value(timestamp.msb);
+        out.value(timestamp.lsb);
         ID_ADAPTER.write(out, timestamp.node);
         out.endArray();
     }
@@ -164,7 +162,7 @@ public class Json
         @Override
         public TxnId read(JsonReader in) throws IOException
         {
-            return readTimestamp(in, TxnId::new);
+            return readTimestamp(in, TxnId::fromBits);
         }
     };
 
@@ -179,7 +177,7 @@ public class Json
         @Override
         public Ballot read(JsonReader in) throws IOException
         {
-            return readTimestamp(in, Ballot::new);
+            return readTimestamp(in, Ballot::fromBits);
         }
     };
 
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromAgent.java b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromAgent.java
index 729fa89..598c632 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromAgent.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromAgent.java
@@ -60,6 +60,6 @@ public class MaelstromAgent implements Agent
     @Override
     public boolean isExpired(TxnId initiated, long now)
     {
-        return TimeUnit.SECONDS.convert(now - initiated.real, TimeUnit.MICROSECONDS) >= 10;
+        return TimeUnit.SECONDS.convert(now - initiated.hlc(), TimeUnit.MICROSECONDS) >= 10;
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org