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 2026/09/15 13:28:59 UTC

(cassandra-accord) branch trunk updated: Accord: Compute Dependencies Incrementally

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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new 56b7710a Accord: Compute Dependencies Incrementally
56b7710a is described below

commit 56b7710afaf3ac1492e50669f8ef5c0985654586
Author: Benedict Elliott Smith <be...@apache.org>
AuthorDate: Sun Sep 6 21:25:04 2026 +0100

    Accord: Compute Dependencies Incrementally
    
    Improve tail latency by using the new incremental execution machinery for computing dependency sets, so that the command store lock is not held for long periods by sync points.
    
    patch by Benedict; reviewed by Aleksey Yeschenko for CASSANDRA-21682
---
 .../accord/coordinate/AbstractCoordination.java    |   6 +-
 .../java/accord/coordinate/CollectLatestDeps.java  |   9 +-
 .../accord/coordinate/CoordinateTransaction.java   |  89 +++----
 .../java/accord/impl/AbstractSafeCommandStore.java | 162 +++++++-----
 .../java/accord/impl/InMemoryCommandStore.java     |  24 +-
 .../src/main/java/accord/local/CommandStore.java   |   4 +-
 .../src/main/java/accord/local/CommandStores.java  |   6 +-
 .../main/java/accord/local/CommandSummaries.java   |  69 +++--
 .../src/main/java/accord/local/DepsCalculator.java | 279 ++++++++++++++++++---
 .../main/java/accord/local/ExecutionContext.java   |  48 ++--
 .../local/{LoadKeysFor.java => FindKeys.java}      |  16 +-
 .../src/main/java/accord/local/LoadKeys.java       |  14 +-
 .../java/accord/local/MapReduceCommandStores.java  |   2 +
 .../main/java/accord/local/SafeCommandStore.java   |  17 +-
 .../accord/local/durability/DurabilityLevel.java   |   4 +-
 .../src/main/java/accord/messages/Accept.java      | 195 ++++++++++----
 .../src/main/java/accord/messages/Apply.java       |   2 +-
 .../accord/messages/ApplyThenWaitUntilApplied.java |   8 +
 .../java/accord/messages/BeginInvalidation.java    |   6 +
 .../main/java/accord/messages/BeginRecovery.java   |  31 ++-
 .../java/accord/messages/GetEphemeralReadDeps.java |  74 ++++--
 .../main/java/accord/messages/GetLatestDeps.java   | 101 +++++---
 .../main/java/accord/messages/GetMaxConflict.java  |   6 +
 .../main/java/accord/messages/InformDurable.java   |   6 +
 .../main/java/accord/messages/NoWaitRequest.java   |  38 +--
 .../java/accord/messages/ParticipantsRequest.java  |   2 +-
 .../src/main/java/accord/messages/PreAccept.java   |  96 ++++---
 .../src/main/java/accord/messages/ReadData.java    |   9 +-
 .../src/main/java/accord/messages/ReplyList.java   |  93 +++++++
 .../main/java/accord/messages/RouteRequest.java    |   4 +-
 .../src/main/java/accord/primitives/Deps.java      |  28 +--
 .../main/java/accord/utils/async/AsyncChain.java   |   7 +-
 .../main/java/accord/utils/async/AsyncChains.java  |   1 -
 .../main/java/accord/utils/async/AsyncResults.java |  66 ++++-
 .../accord/utils/async/CancellableAsyncResult.java |  23 ++
 35 files changed, 1098 insertions(+), 447 deletions(-)

diff --git a/accord-core/src/main/java/accord/coordinate/AbstractCoordination.java b/accord-core/src/main/java/accord/coordinate/AbstractCoordination.java
index 0aaec1b0..cee085cf 100644
--- a/accord-core/src/main/java/accord/coordinate/AbstractCoordination.java
+++ b/accord-core/src/main/java/accord/coordinate/AbstractCoordination.java
@@ -461,7 +461,7 @@ public abstract class AbstractCoordination<P extends Participants<?>, Result, Re
     }
 
     enum LocalExecuteState { PENDING, SUCCESS, TIMEOUT }
-    abstract class AbstractLocalExecute extends MapReduceConsumeCommandStores<P, Reply> implements Timeouts.Timeout
+    abstract class AbstractLocalExecute<R> extends MapReduceConsumeCommandStores<P, R> implements Timeouts.Timeout
     {
         LocalExecuteState state = PENDING;
         Cancellable cancel;
@@ -469,7 +469,7 @@ public abstract class AbstractCoordination<P extends Participants<?>, Result, Re
 
         abstract long expiresAt();
         abstract Cancellable submit();
-        abstract void acceptInternal(Reply result, Throwable failure);
+        abstract void acceptInternal(R result, Throwable failure);
 
         protected AbstractLocalExecute()
         {
@@ -504,7 +504,7 @@ public abstract class AbstractCoordination<P extends Participants<?>, Result, Re
         }
 
         @Override
-        public void accept(Reply result, Throwable failure)
+        public void accept(R result, Throwable failure)
         {
             done();
             executor.executeMaybeImmediately(() -> acceptInternal(result, failure));
diff --git a/accord-core/src/main/java/accord/coordinate/CollectLatestDeps.java b/accord-core/src/main/java/accord/coordinate/CollectLatestDeps.java
index f02d2240..6b1329c7 100644
--- a/accord-core/src/main/java/accord/coordinate/CollectLatestDeps.java
+++ b/accord-core/src/main/java/accord/coordinate/CollectLatestDeps.java
@@ -29,7 +29,6 @@ import accord.coordinate.tracking.QuorumTracker;
 import accord.local.Node;
 import accord.local.Node.Id;
 import accord.messages.GetLatestDeps;
-import accord.messages.GetLatestDeps.GetLatestDepsOk;
 import accord.messages.GetLatestDeps.GetLatestDepsReply;
 import accord.primitives.Ballot;
 import accord.primitives.FullRoute;
@@ -46,7 +45,7 @@ import static accord.coordinate.tracking.RequestStatus.Failed;
 import static accord.coordinate.tracking.RequestStatus.Success;
 import static accord.primitives.Routables.Slice.Minimal;
 
-public class CollectLatestDeps extends AbstractCoordination<Route<?>, List<LatestDeps>, GetLatestDepsReply, GetLatestDepsOk>
+public class CollectLatestDeps extends AbstractCoordination<Route<?>, List<LatestDeps>, GetLatestDepsReply, GetLatestDepsReply>
 {
     final Timestamp executeAt;
     final @Nullable Ballot ballot;
@@ -90,7 +89,7 @@ public class CollectLatestDeps extends AbstractCoordination<Route<?>, List<Lates
     {
         if (ok.isOk())
         {
-            recordOk(fromIndex, (GetLatestDepsOk) ok);
+            recordOk(fromIndex, ok);
             if (tracker.recordSuccess(from) == Success)
                 onQuorum();
         }
@@ -111,9 +110,9 @@ public class CollectLatestDeps extends AbstractCoordination<Route<?>, List<Lates
     private void onQuorum()
     {
         Invariants.require(!isDone());
-        SortedListMap<Node.Id, GetLatestDepsOk> oks = finishOks();
+        SortedListMap<Node.Id, GetLatestDepsReply> oks = finishOks();
         List<LatestDeps> result = new ArrayList<>(oks.size());
-        for (GetLatestDepsOk ok : oks.values())
+        for (GetLatestDepsReply ok : oks.values())
             result.add(ok.deps);
         finishWithSuccess(result);
     }
diff --git a/accord-core/src/main/java/accord/coordinate/CoordinateTransaction.java b/accord-core/src/main/java/accord/coordinate/CoordinateTransaction.java
index b4cf9ba4..dc25e6c9 100644
--- a/accord-core/src/main/java/accord/coordinate/CoordinateTransaction.java
+++ b/accord-core/src/main/java/accord/coordinate/CoordinateTransaction.java
@@ -20,33 +20,33 @@ package accord.coordinate;
 
 import java.util.List;
 import java.util.function.BiConsumer;
-
 import javax.annotation.Nullable;
 
+import accord.api.ExclusiveAsyncExecutor;
 import accord.api.ProtocolModifiers;
 import accord.api.Result;
 import accord.coordinate.CoordinationAdapter.Adapters;
 import accord.coordinate.ExecuteFlag.CoordinationFlags;
 import accord.coordinate.ExecuteFlag.ExecuteFlags;
 import accord.local.Commands;
-import accord.local.DepsCalculator;
 import accord.local.LoadKeys;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
+import accord.local.Node;
 import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
-import accord.api.ExclusiveAsyncExecutor;
 import accord.local.StoreParticipants;
+import accord.messages.PreAccept.PreAcceptDepsCalculator;
 import accord.messages.PreAccept.PreAcceptNack;
-import accord.messages.PreAccept.PreAcceptReply;
-import accord.topology.Topologies;
-import accord.local.Node;
 import accord.messages.PreAccept.PreAcceptOk;
+import accord.messages.PreAccept.PreAcceptReply;
+import accord.messages.ReplyList;
 import accord.primitives.Ballot;
 import accord.primitives.Deps;
 import accord.primitives.FullRoute;
 import accord.primitives.Timestamp;
 import accord.primitives.Txn;
 import accord.primitives.TxnId;
+import accord.topology.Topologies;
 import accord.utils.SortedListMap;
 import accord.utils.async.AsyncChain;
 import accord.utils.async.AsyncChains;
@@ -60,8 +60,8 @@ import static accord.messages.Accept.Kind.MEDIUM;
 import static accord.messages.Accept.Kind.SLOW;
 import static accord.messages.MessageType.StandardMessage.PRE_ACCEPT_REQ;
 import static accord.primitives.Timestamp.Flag.REJECTED;
-import static accord.primitives.Timestamp.mergeMaxAndFlags;
 import static accord.primitives.Timestamp.Flag.SOFT_REJECT;
+import static accord.primitives.Timestamp.mergeMaxAndFlags;
 import static accord.primitives.TxnId.FastPath.PrivilegedCoordinatorWithDeps;
 import static accord.topology.SelectShards.LIVE;
 import static java.util.concurrent.TimeUnit.MICROSECONDS;
@@ -209,7 +209,7 @@ public class CoordinateTransaction extends CoordinatePreAccept<Result>
         return node.coordinationAdapter(txnId, Standard);
     }
 
-    class LocalExecute extends AbstractLocalExecute
+    class LocalExecute extends AbstractLocalExecute<ReplyList<PreAcceptReply>>
     {
         @Override
         long expiresAt()
@@ -224,7 +224,7 @@ public class CoordinateTransaction extends CoordinatePreAccept<Result>
         }
 
         @Override
-        public void acceptInternal(PreAcceptReply result, Throwable failure)
+        public void acceptInternal(ReplyList<PreAcceptReply> replies, Throwable failure)
         {
             if (failure != null)
             {
@@ -236,56 +236,49 @@ public class CoordinateTransaction extends CoordinatePreAccept<Result>
             }
             else
             {
-                if (result.isOk())
-                {
-                    PreAcceptOk ok = (PreAcceptOk) result;
-                    // TODO (desired): we can probably still process and record fast path votes from peers, just with different quorum requirements
-                    boolean hasCoordinatorVote = txnId.equals(ok.witnessedAt);
-                    if (!hasCoordinatorVote && txnId.hasPrivilegedCoordinator()) fastPathEnabled = false;
-                    Deps deps = hasCoordinatorVote && txnId.is(PrivilegedCoordinatorWithDeps) ? ok.deps : null;
-                    contactNotSelf(deps, hasCoordinatorVote);
-                    onSuccess(node.id(), ok);
-                }
-                else
-                {
-                    finishOnFailure(Preempted.preempted(node.agent(), txnId, scope.homeKey()));
-                }
+                ReplyList.invoke(replies, PreAcceptReply::reduce, (success, fail) -> {
+                    if (success.isOk())
+                    {
+                        PreAcceptOk ok = (PreAcceptOk) success;
+                        boolean hasCoordinatorVote = txnId.equals(ok.witnessedAt);
+                        if (!hasCoordinatorVote && txnId.hasPrivilegedCoordinator()) fastPathEnabled = false;
+                        Deps deps = hasCoordinatorVote && txnId.is(PrivilegedCoordinatorWithDeps) ? ok.deps : null;
+                        contactNotSelf(deps, hasCoordinatorVote);
+                        onSuccess(node.id(), ok);
+                    }
+                    else
+                    {
+                        finishOnFailure(Preempted.preempted(node.agent(), txnId, scope.homeKey()));
+                    }
+                });
             }
         }
 
         @Override
-        public PreAcceptReply applyInternal(SafeCommandStore safeStore)
+        public ReplyList<PreAcceptReply> applyInternal(SafeCommandStore safeStore)
         {
             long minEpoch = topologies.oldestEpoch();
             StoreParticipants participants = StoreParticipants.update(safeStore, scope, minEpoch, txnId, txnId.epoch());
             SafeCommand safeCommand = safeStore.get(txnId, participants);
 
-            Timestamp executeAt;
-            Deps deps;
-            ExecuteFlags flags;
-            try (DepsCalculator calculator = new DepsCalculator(txnId))
-            {
-                deps = calculator.calculate(safeStore, txnId, participants, minEpoch, txnId, true);
-                if (deps == null)
-                    return PreAcceptNack.INSTANCE;
-
-                boolean hasCoordinatorVote = txnId.hasPrivilegedCoordinator();
-                Deps coordinatorDeps = txnId.is(PrivilegedCoordinatorWithDeps) ? deps : null;
-                Commands.AcceptOutcome outcome = Commands.preaccept(safeStore, safeCommand, participants, txnId, txn, coordinatorDeps, hasCoordinatorVote);
-                if (outcome != Success)
-                    return PreAcceptNack.INSTANCE;
-
-                executeAt = calculator.executeAt(safeCommand, node);
-                flags = calculator.executeFlags(txnId);
-            }
+            boolean hasCoordinatorVote = txnId.hasPrivilegedCoordinator();
+            Commands.AcceptOutcome outcome = Commands.preaccept(safeStore, safeCommand, participants, txnId, txn, null, hasCoordinatorVote);
+            if (outcome != Success)
+                return PreAcceptNack.INSTANCE;
 
-            return new PreAcceptOk(txnId, executeAt, deps, flags);
+            Timestamp witnessedAt = safeCommand.current().executeAt;
+            //noinspection resource
+            PreAcceptDepsCalculator calculator = new PreAcceptDepsCalculator(txnId, witnessedAt, participants, node);
+            ReplyList<PreAcceptReply> reply = calculator.calculate(safeStore, minEpoch, true);
+            if (reply == null)
+                reply = PreAcceptNack.INSTANCE;
+            return reply;
         }
 
         @Override
-        public PreAcceptReply reduce(PreAcceptReply r1, PreAcceptReply r2)
+        public ReplyList<PreAcceptReply> reduce(ReplyList<PreAcceptReply> r1, ReplyList<PreAcceptReply> r2)
         {
-            return PreAcceptReply.reduce(r1, r2);
+            return ReplyList.merge(r1, r2);
         }
 
         @Override
@@ -295,9 +288,9 @@ public class CoordinateTransaction extends CoordinatePreAccept<Result>
         }
 
         @Override
-        public LoadKeysFor loadKeysFor()
+        public FindKeys findKeys()
         {
-            return LoadKeysFor.READ_WRITE;
+            return FindKeys.CONFLICTS;
         }
 
         public ExecutionKind executionKind()
diff --git a/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java b/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
index 6fd9e368..213b6180 100644
--- a/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/AbstractSafeCommandStore.java
@@ -22,7 +22,11 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.NavigableMap;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import accord.api.RoutingKey;
+import accord.local.ExecutionContext.OverrideKeys;
 import accord.local.LoadKeys;
 import accord.local.ExecutionContext;
 import accord.local.RedundantBefore;
@@ -30,7 +34,7 @@ import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
 import accord.local.cfk.SafeCommandsForKey;
 import accord.primitives.Ranges;
-import accord.primitives.Routable;
+import accord.primitives.Routable.Domain;
 import accord.primitives.RoutingKeys;
 import accord.primitives.Timestamp;
 import accord.primitives.TxnId;
@@ -67,81 +71,95 @@ extends SafeCommandStore
     @Override
     public ExecutionContext canExecute(ExecutionContext with)
     {
-        if (with.isEmpty()) return with;
-        if (with.keys().domain() == Routable.Domain.Range)
-            return with.isSubsetOf(this.context) ? with : null;
-
-        LoadKeys require = with.loadKeys();
-        if (require != LoadKeys.NONE)
-        {
-            ExecutionContext context = context();
-            if (!context.loadKeys().satisfiesIfPresent(require))
-                return null;
+        Unseekables<?> withKeys = with.keys();
+        if (withKeys.domain() == Domain.Range)
+            return with.isSubsetOf(context) ? with : null;
 
-            if (with.loadKeysFor().compareTo(context.loadKeysFor()) > 0)
-                return null;
-        }
+        LoadKeys loadKeys = with.loadKeys();
+        if (loadKeys != LoadKeys.NONE && with.findKeys().compareTo(context.findKeys()) > 0)
+            return null;
 
-        try (Caches caches = tryGetCaches())
+        Caches caches = null;
+        try
         {
-            for (TxnId txnId : with.txnIds())
+            TxnId primaryTxnId = with.primaryTxnId();
+            if (primaryTxnId != null)
             {
-                if (null != getInternal(txnId))
-                    continue;
-
-                if (caches == null)
-                    return null;
+                if (!isPresent(primaryTxnId))
+                {
+                    caches = tryGetCaches();
+                    if (ifLoadedInternal(primaryTxnId) == null)
+                        return null;
+                }
 
-                C safeCommand = caches.acquireIfLoaded(txnId);
-                if (safeCommand == null)
-                    return null;
+                TxnId additionalTxnId = with.additionalTxnId();
+                if (additionalTxnId != null && !isPresent(additionalTxnId))
+                {
+                    if (caches == null)
+                        caches = tryGetCaches();
 
-                add(safeCommand, caches);
+                    if (ifLoadedInternal(additionalTxnId) == null)
+                        return null;
+                }
             }
 
-            LoadKeys loadKeys = with.loadKeys();
-            if (loadKeys == LoadKeys.NONE)
+            if (loadKeys == LoadKeys.NONE || withKeys.isEmpty())
                 return with;
 
             List<RoutingKey> unavailable = null;
-            Unseekables<?> keys = with.keys();
-            if (keys.isEmpty())
-                return with;
-
-            for (int i = 0 ; i < keys.size() ; ++i)
+            for (int i = 0 ; i < withKeys.size() ; ++i)
             {
-                RoutingKey key = (RoutingKey) keys.get(i);
-                if (null != getInternal(key))
-                    continue; // already in working set
+                RoutingKey key = (RoutingKey) withKeys.get(i);
+                if (isPresent(key))
+                    continue;
+
+                if (unavailable == null && caches == null)
+                    caches = tryGetCaches();
+
+                if (ifLoadedInternal(caches, key) != null)
+                    continue;
 
-                if (caches != null)
-                {
-                    CFK safeCfk = caches.acquireIfLoaded(key);
-                    if (safeCfk != null)
-                    {
-                        add(safeCfk, caches);
-                        continue;
-                    }
-                }
                 if (unavailable == null)
                     unavailable = new ArrayList<>();
+
                 unavailable.add(key);
             }
 
             if (unavailable == null)
                 return with;
 
-            if (unavailable.size() == keys.size())
+            if (unavailable.size() == withKeys.size())
                 return null;
 
-            return ExecutionContext.contextFor(with.primaryTxnId(), with.additionalTxnId(), keys.without(RoutingKeys.ofSortedUnique(unavailable)), loadKeys, context.loadKeysFor(), context.reason());
+            return new OverrideKeys(with, withKeys.without(RoutingKeys.ofSortedUnique(unavailable)));
+        }
+        finally
+        {
+            if (caches != null)
+                caches.close();
         }
     }
 
-    @Override
-    public ExecutionContext context()
+    private boolean isPresent(TxnId txnId)
     {
-        return context;
+        return getInternal(txnId) != null;
+    }
+
+    private boolean isPresentOrLoadedInternal(@Nonnull Caches caches, TxnId txnId)
+    {
+        return getInternal(txnId) != null || ifLoadedInternal(caches, txnId) != null;
+    }
+
+    private C ifLoadedInternal(@Nullable Caches caches, TxnId txnId)
+    {
+        if (caches == null)
+            return null;
+
+        C command = caches.acquireIfLoaded(txnId);
+        if (command == null)
+            return null;
+
+        return add(command, caches);
     }
 
     @Override
@@ -149,33 +167,47 @@ extends SafeCommandStore
     {
         try (Caches caches = tryGetCaches())
         {
-            if (caches == null)
-                return null;
+            return ifLoadedInternal(caches, txnId);
+        }
+    }
 
-            C command = caches.acquireIfLoaded(txnId);
-            if (command == null)
-                return null;
+    private boolean isPresentOrLoadedInternal(@Nonnull Caches caches, RoutingKey key)
+    {
+        return getInternal(key) != null || ifLoadedInternal(caches, key) != null;
+    }
 
-            return add(command, caches);
-        }
+    private boolean isPresent(RoutingKey key)
+    {
+        return getInternal(key) != null;
+    }
+
+    protected CFK ifLoadedInternal(@Nullable Caches caches, RoutingKey key)
+    {
+        if (caches == null)
+            return null;
+
+        CFK cfk = caches.acquireIfLoaded(key);
+        if (cfk == null)
+            return null;
+
+        return add(cfk, caches);
     }
 
     @Override
-    protected CFK ifLoadedInternal(RoutingKey txnId)
+    protected CFK ifLoadedInternal(RoutingKey key)
     {
         try (Caches caches = tryGetCaches())
         {
-            if (caches == null)
-                return null;
-
-            CFK cfk = caches.acquireIfLoaded(txnId);
-            if (cfk == null)
-                return null;
-
-            return add(cfk, caches);
+            return ifLoadedInternal(caches, key);
         }
     }
 
+    @Override
+    public ExecutionContext context()
+    {
+        return context;
+    }
+
     // TODO (expected): cleanup the integration hooks here; they're a bit byzantine. Also clearly document behaviour.
     public void postExecute()
     {
diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
index 62b1757f..b413c01b 100644
--- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
+++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java
@@ -67,7 +67,7 @@ import accord.local.CommandSummaries;
 import accord.local.CommandSummaries.Summary;
 import accord.local.CommandSummaries.SummaryLoader;
 import accord.local.Commands;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
 import accord.local.MaxDecidedRX;
 import accord.local.NodeCommandStoreService;
 import accord.local.ExecutionContext;
@@ -92,8 +92,8 @@ import static accord.api.ProtocolModifiers.isRangeEndInclusive;
 import static accord.api.ProtocolModifiers.isRangeStartInclusive;
 import static accord.local.Cleanup.Input.FULL;
 import static accord.local.LoadKeys.NONE;
-import static accord.local.LoadKeysFor.RECOVERY;
-import static accord.local.LoadKeysFor.WRITE;
+import static accord.local.FindKeys.SUPERSEDING;
+import static accord.local.FindKeys.DECLARED;
 import static accord.local.RedundantStatus.Coverage.ALL;
 import static accord.local.StoreParticipants.Filter.LOAD;
 import static accord.primitives.Routable.Domain.Key;
@@ -456,7 +456,7 @@ public abstract class InMemoryCommandStore extends CommandStore
                     commandsForKey.put(key, safeCfk);
                 }
             }
-            else if (context.loadKeysFor() != WRITE)
+            else if (context.findKeys() != DECLARED)
             {
                 SummaryLoader loader = SummaryLoader.loader(unsafeGetRedundantBefore(), unsafeGetMaxDecidedRX(), context);
                 for (GlobalCommandsForKey global : this.commandsForKey.values())
@@ -839,7 +839,7 @@ public abstract class InMemoryCommandStore extends CommandStore
             if (commandsForRanges != null)
                 return commandsForRanges;
 
-            Invariants.require(context.loadKeysFor() != WRITE);
+            Invariants.require(context.findKeys() != DECLARED);
             MaxDecidedRX maxDecidedRX = commandStore().unsafeGetMaxDecidedRX();
             SummaryLoader loader = cfrLoad != null ? cfrLoad.loader
                                                    : SummaryLoader.loader(redundantBefore(), maxDecidedRX, context);
@@ -852,11 +852,11 @@ public abstract class InMemoryCommandStore extends CommandStore
             return commandsForRanges = () -> loaded;
         }
 
-        private boolean visitForKey(Unseekables<?> keysOrRanges, Predicate<CommandsForKey> forEach)
+        private boolean visitForKey(@Nullable Unseekables<?> keysOrRanges, Predicate<CommandsForKey> forEach)
         {
             for (SafeCommandsForKey safeCfk : commandsForKey.values())
             {
-                if (!keysOrRanges.contains(safeCfk.key()))
+                if (keysOrRanges != null && !keysOrRanges.contains(safeCfk.key()))
                     continue;
 
                 if (!forEach.test(safeCfk.current()))
@@ -865,18 +865,18 @@ public abstract class InMemoryCommandStore extends CommandStore
             return true;
         }
 
-        private <P1, P2> void visitForKey(Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visitor, P1 p1, P2 p2)
+        private <P1, P2> void visitForKey(@Nullable Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visitor, P1 p1, P2 p2)
         {
             visitForKey(keysOrRanges, cfk -> { cfk.visit(startedBefore, testKind, visitor, p1, p2); return true; });
         }
 
-        public boolean visitForKey(Unseekables<?> keysOrRanges, TxnId testTxnId, Kinds testKind, SupersedingCommandVisitor visit)
+        public boolean visitForKey(@Nullable Unseekables<?> keysOrRanges, TxnId testTxnId, Kinds testKind, SupersedingCommandVisitor visit)
         {
             return visitForKey(keysOrRanges, cfk -> cfk.visit(testTxnId, testKind, visit));
         }
 
         @Override
-        public <P1, P2> void visit(Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visitor, P1 p1, P2 p2)
+        public <P1, P2> void visit(@Nullable Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visitor, P1 p1, P2 p2)
         {
             visitForKey(keysOrRanges, startedBefore, testKind, visitor, p1, p2);
             commandsForRanges().visit(keysOrRanges, startedBefore, testKind, visitor, p1, p2);
@@ -937,14 +937,14 @@ public abstract class InMemoryCommandStore extends CommandStore
 
     protected CommandsForRangeLoad cfrLoad(ExecutionContext context)
     {
-        if (context.loadKeysFor() != LoadKeysFor.RECOVERY)
+        if (context.findKeys() != FindKeys.SUPERSEDING)
             return null;
 
         SummaryLoader loader = SummaryLoader.loader(unsafeGetRedundantBefore(), unsafeGetMaxDecidedRX(), context);
         commandsForRanges.populateMinFutureRx(loader);
         TreeMap<Timestamp, Summary> loaded = new TreeMap<>();
         commandsForRanges.search(loader, null, txnId -> {
-            Invariants.require(loader.loadKeysFor() == RECOVERY);
+            Invariants.require(loader.loadKeysFor() == SUPERSEDING);
             Command command = commands.get(txnId).value();
             Summary summary = loader.ifRelevant(command);
             // TODO (expected): prune implied invalidations from index, so no need to special case
diff --git a/accord-core/src/main/java/accord/local/CommandStore.java b/accord-core/src/main/java/accord/local/CommandStore.java
index 20d9b4fe..12d9a6d9 100644
--- a/accord-core/src/main/java/accord/local/CommandStore.java
+++ b/accord-core/src/main/java/accord/local/CommandStore.java
@@ -64,6 +64,8 @@ import accord.primitives.Status.Durability.HasOutcome;
 import accord.utils.DeterministicIdentitySet;
 import accord.utils.Invariants;
 import accord.utils.Reduce;
+import accord.utils.SortedArrays;
+import accord.utils.SortedArrays.SortedArrayList;
 import accord.utils.UnhandledEnum;
 import accord.utils.async.AsyncChain;
 import accord.utils.async.AsyncChains;
@@ -525,7 +527,7 @@ public abstract class CommandStore implements AbstractAsyncExecutor, ExclusiveAs
         dataStore.ensureDurable(this, ranges, addOnDataStoreDurable, 0);
         ensureDurable(ranges, addOnCommandStoreDurable);
         Ranges unavailable = unavailable(txnId, txnIdWithFlags, ranges, safeStore.ranges(), safeStore.safeToReadAt());
-        node.durability().report(new DurabilityResult(new MinimalSyncPoint(txnId, txnIdWithFlags, route.without(unavailable)), new DurabilityLevel(Self, NoRemote, null), null));
+        node.durability().report(new DurabilityResult(new MinimalSyncPoint(txnId, txnIdWithFlags, route.without(unavailable)), new DurabilityLevel(Self, NoRemote, SortedArrayList.ofSorted(node.id())), null));
     }
 
     /**
diff --git a/accord-core/src/main/java/accord/local/CommandStores.java b/accord-core/src/main/java/accord/local/CommandStores.java
index ef9ce1a4..d0896cbc 100644
--- a/accord-core/src/main/java/accord/local/CommandStores.java
+++ b/accord-core/src/main/java/accord/local/CommandStores.java
@@ -1068,15 +1068,15 @@ public abstract class CommandStores implements AsyncExecutorFactory
 
     public AsyncChain<Void> forEach(String reason, TxnId txnId, Participants<?> participants, long minEpoch, long maxEpoch, Consumer<SafeCommandStore> forEach)
     {
-        return forEach(reason, txnId, participants, LoadKeys.SYNC, LoadKeysFor.READ_WRITE,  minEpoch, maxEpoch, forEach);
+        return forEach(reason, txnId, participants, LoadKeys.SYNC, FindKeys.CONFLICTS, minEpoch, maxEpoch, forEach);
     }
 
-    public AsyncChain<Void> forEach(String reason, TxnId txnId, Participants<?> participants, LoadKeys loadKeys, LoadKeysFor loadKeysFor, long minEpoch, long maxEpoch, Consumer<SafeCommandStore> forEach)
+    public AsyncChain<Void> forEach(String reason, TxnId txnId, Participants<?> participants, LoadKeys loadKeys, FindKeys findKeys, long minEpoch, long maxEpoch, Consumer<SafeCommandStore> forEach)
     {
         return mapReduce(StoreFinder.selector(participants, minEpoch, maxEpoch), new MapReduceCommandStores<Participants<?>, Void>(participants)
         {
             @Override public LoadKeys loadKeys() { return loadKeys;}
-            @Override public LoadKeysFor loadKeysFor() { return loadKeysFor; }
+            @Override public FindKeys findKeys() { return findKeys; }
             @Override public Void reduce(Void o1, Void o2) { return null; }
             @Override public TxnId primaryTxnId() { return txnId; }
             @Override public String reason() { return reason; }
diff --git a/accord-core/src/main/java/accord/local/CommandSummaries.java b/accord-core/src/main/java/accord/local/CommandSummaries.java
index d1c1de98..a710f04e 100644
--- a/accord-core/src/main/java/accord/local/CommandSummaries.java
+++ b/accord-core/src/main/java/accord/local/CommandSummaries.java
@@ -53,8 +53,7 @@ import static accord.local.CommandSummaries.SummaryStatus.ACCEPTED;
 import static accord.local.CommandSummaries.SummaryStatus.APPLIED;
 import static accord.local.CommandSummaries.SummaryStatus.NOTACCEPTED;
 import static accord.local.CommandSummaries.SummaryStatus.PREACCEPTED;
-import static accord.local.LoadKeysFor.RECOVERY;
-import static accord.local.LoadKeysFor.WRITE;
+import static accord.local.FindKeys.DECLARED;
 import static accord.local.MaxDecidedRX.forDeps;
 import static accord.primitives.Known.KnownDeps.NoDeps;
 import static accord.primitives.Routables.Slice.Minimal;
@@ -102,7 +101,6 @@ public interface CommandSummaries
 
         private static final Relevance[] lookup = values();
         public static final int ENCODED_MASK = 7;
-        public static final int ENCODED_BITS = 3;
         private final int encoded;
 
         Relevance(int encoded)
@@ -238,7 +236,7 @@ public interface CommandSummaries
     {
         public interface Factory<L extends SummaryLoader>
         {
-            L create(RedundantBefore redundantBefore, @Nullable MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, Kinds testKind, TxnId minTxnId, Timestamp executeAt, LoadKeysFor loadKeysFor);
+            L create(RedundantBefore redundantBefore, @Nullable MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Unseekables<?> searchKeysOrRanges, Kinds testKind, TxnId minTxnId, Timestamp executeAt, FindKeys findKeys);
         }
 
         private static final ReducingRangeMap<TxnId> NO_RX = new ReducingRangeMap<>();
@@ -248,7 +246,7 @@ public interface CommandSummaries
         protected final Unseekables<?> searchFor;
         // TODO (expected): separate out Kinds we need before/after primaryTxnId/executeAt
         protected final Kinds testKind;
-        protected final LoadKeysFor loadKeysFor;
+        protected final FindKeys findKeys;
         protected final TxnId primaryTxnId, minTxnId;
         protected final DecidedRX decidedRx;
         protected final Timestamp primaryExecuteAt;
@@ -259,30 +257,30 @@ public interface CommandSummaries
         // TODO (expected): provide executeAt to PreLoadContext so we can more aggressively filter what we load, esp. by Kind
         public static SummaryLoader loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, ExecutionContext context)
         {
-            return loader(redundantBefore, maxDecidedRX, context.primaryTxnId(), context.executeAt(), context.loadKeysFor(), context.keys());
+            return loader(redundantBefore, maxDecidedRX, context.primaryTxnId(), context.executeAt(), context.findKeys(), context.keys());
         }
 
-        public static SummaryLoader loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Timestamp executeAt, LoadKeysFor loadKeysFor, Unseekables<?> keysOrRanges)
+        public static SummaryLoader loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Timestamp executeAt, FindKeys findKeys, Unseekables<?> keysOrRanges)
         {
-            return loader(redundantBefore, maxDecidedRX, primaryTxnId, executeAt, loadKeysFor, keysOrRanges, SummaryLoader::new);
+            return loader(redundantBefore, maxDecidedRX, primaryTxnId, executeAt, findKeys, keysOrRanges, SummaryLoader::new);
         }
 
         public static <L extends SummaryLoader> L loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, ExecutionContext context, Factory<L> factory)
         {
-            return loader(redundantBefore, maxDecidedRX, context.primaryTxnId(), context.executeAt(), context.loadKeysFor(), context.keys(), factory);
+            return loader(redundantBefore, maxDecidedRX, context.primaryTxnId(), context.executeAt(), context.findKeys(), context.keys(), factory);
         }
 
-        public static <L extends SummaryLoader> L loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Timestamp executeAt, LoadKeysFor loadKeysFor, Unseekables<?> keysOrRanges, Factory<L> factory)
+        public static <L extends SummaryLoader> L loader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Timestamp executeAt, FindKeys findKeys, Unseekables<?> keysOrRanges, Factory<L> factory)
         {
             Invariants.require(primaryTxnId != null);
             TxnId minTxnId = redundantBefore.min(keysOrRanges, Bounds::gcBefore);
-            Kinds kinds = primaryTxnId.witnesses().or(loadKeysFor == RECOVERY ? primaryTxnId.witnessedBy() : Nothing);
+            Kinds kinds = primaryTxnId.witnesses().or(findKeys == FindKeys.SUPERSEDING ? primaryTxnId.witnessedBy() : Nothing);
             if (!primaryTxnId.is(Txn.Kind.ExclusiveSyncPoint)) // the main distinction between RX and RV is that RV doesn't filter out decided transactions
                 maxDecidedRX = null;
-            return factory.create(redundantBefore, maxDecidedRX, primaryTxnId, keysOrRanges, kinds, minTxnId, executeAt, loadKeysFor);
+            return factory.create(redundantBefore, maxDecidedRX, primaryTxnId, keysOrRanges, kinds, minTxnId, executeAt, findKeys);
         }
 
-        public SummaryLoader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Unseekables<?> searchFor, Kinds testKind, TxnId minTxnId, Timestamp primaryExecuteAt, LoadKeysFor loadKeysFor)
+        public SummaryLoader(RedundantBefore redundantBefore, MaxDecidedRX maxDecidedRX, TxnId primaryTxnId, Unseekables<?> searchFor, Kinds testKind, TxnId minTxnId, Timestamp primaryExecuteAt, FindKeys findKeys)
         {
             this.redundantBefore = redundantBefore;
             this.maxDecidedRX = maxDecidedRX;
@@ -291,7 +289,7 @@ public interface CommandSummaries
             this.testKind = testKind;
             this.minTxnId = minTxnId;
             this.primaryExecuteAt = primaryExecuteAt;
-            this.loadKeysFor = loadKeysFor;
+            this.findKeys = findKeys;
             this.decidedRx = forDeps(maxDecidedRX, searchFor, primaryTxnId);
         }
 
@@ -300,9 +298,9 @@ public interface CommandSummaries
             return searchFor;
         }
 
-        public LoadKeysFor loadKeysFor()
+        public FindKeys loadKeysFor()
         {
-            return loadKeysFor;
+            return findKeys;
         }
 
         public TxnId primaryTxnId()
@@ -393,7 +391,7 @@ public interface CommandSummaries
 
         private Relevance relevanceInternal(TxnId txnId, @Nullable SaveStatus saveStatus, @Nullable Durability durability, @Nullable Timestamp executeAt, @Nullable Unseekables<?> participants, boolean permitNull)
         {
-            if (loadKeysFor == WRITE || !txnId.is(testKind) || (saveStatus != null && saveStatus.compareTo(SaveStatus.TruncatedUnapplied) >= 0))
+            if (findKeys == DECLARED || !txnId.is(testKind) || (saveStatus != null && saveStatus.compareTo(SaveStatus.TruncatedUnapplied) >= 0))
                 return IRRELEVANT;
 
             if (!permitNull)
@@ -414,7 +412,7 @@ public interface CommandSummaries
                 return IRRELEVANT;
 
             Relevance atLeast = IRRELEVANT;
-            if (loadKeysFor == RECOVERY && txnId.witnesses(primaryTxnId))
+            if (findKeys == FindKeys.SUPERSEDING && txnId.witnesses(primaryTxnId))
             {
                 if (isIgnorableFutureRx(txnId, participants))
                     return IRRELEVANT;
@@ -531,7 +529,7 @@ public interface CommandSummaries
         public final <P> Summary get(Relevance relevance, TxnId txnId, Timestamp executeAt, SaveStatus saveStatus, Durability durability, Participants<?> touches, @Nullable P deps, TriPredicate<P, TxnId, Unseekables<?>> depTester)
         {
             touches = touches.intersecting(searchFor, Minimal);
-            IsDep isDep = loadKeysFor == RECOVERY ? IsDep.NOT_ELIGIBLE : null;
+            IsDep isDep = findKeys == FindKeys.SUPERSEDING ? IsDep.NOT_ELIGIBLE : null;
             switch (relevance)
             {
                 default: throw new UnhandledEnum(relevance);
@@ -569,15 +567,6 @@ public interface CommandSummaries
         }
     }
     
-    enum ComputeIsDep
-    {
-        // don't test deps
-        IGNORE,
-
-        // calculate but don't filter
-        EITHER
-    }
-
     interface ActiveCommandVisitor<P1, P2>
     {
         void visit(P1 p1, P2 p2, SummaryStatus status, Durability durability, Unseekable keyOrRange, TxnId txnId);
@@ -592,20 +581,20 @@ public interface CommandSummaries
         boolean visit(Unseekable keyOrRange, TxnId txnId, Timestamp executeAt, SummaryStatus status, @Nullable IsDep dep, Durability minDurability);
     }
 
-    boolean visit(Unseekables<?> keysOrRanges, TxnId testTxnId, Kinds testKind, SupersedingCommandVisitor visit);
+    boolean visit(@Nullable Unseekables<?> keysOrRanges, TxnId testTxnId, Kinds testKind, SupersedingCommandVisitor visit);
 
     /**
      * Visits keys first in ascending order, with equal keys visiting TxnId is ascending order.
      * Visits range transactions in ascending order by TxnId, then visiting each Range in ascending order
      */
-    <P1, P2> void visit(Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visit, P1 p1, P2 p2);
+    <P1, P2> void visit(@Nullable Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visit, P1 p1, P2 p2);
 
     // TODO (expected): ByRangeSnapshot based on IntervalBTree so we can elide superseded dependencies as we do with CommandsForKey
     interface ByTxnIdSnapshot extends CommandSummaries
     {
         NavigableMap<Timestamp, Summary> byTxnId();
 
-        default boolean visit(Unseekables<?> keysOrRanges,
+        default boolean visit(@Nullable Unseekables<?> keysOrRanges,
                               TxnId testTxnId,
                               Kinds testKind,
                               SupersedingCommandVisitor visit)
@@ -620,15 +609,17 @@ public interface CommandSummaries
                 if (!value.is(MAYBE_SUPERSEDING))
                     continue;
 
-                Unseekables<?> participants = value.participants;
-                Unseekables<?> intersecting = participants.overlapping(keysOrRanges);
-                if (!intersecting.isEmpty())
+                Unseekables<?> overlapping = value.participants;
+                if (keysOrRanges != null)
+                    overlapping = overlapping.overlapping(keysOrRanges);
+
+                if (!overlapping.isEmpty())
                 {
                     Timestamp executeAt = value.plainExecuteAt();
                     Invariants.require(executeAt != null);
                     SummaryStatus status = value.status();
                     IsDep dep = value.isDep();
-                    for (Unseekable participant : intersecting)
+                    for (Unseekable participant : overlapping)
                     {
                         if (!visit.visit(participant, value.plainTxnId(), executeAt, status, dep, NotDurable))
                             return false;
@@ -640,7 +631,7 @@ public interface CommandSummaries
         }
 
         @Override
-        default <P1, P2> void visit(Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visit, P1 p1, P2 p2)
+        default <P1, P2> void visit(@Nullable Unseekables<?> keysOrRanges, Timestamp startedBefore, Kinds testKind, ActiveCommandVisitor<P1, P2> visit, P1 p1, P2 p2)
         {
             NavigableMap<Timestamp, Summary> map = byTxnId();
             for (Summary value : map.headMap(startedBefore, false).values())
@@ -654,7 +645,11 @@ public interface CommandSummaries
                 if (!value.is(ACTIVE))
                     continue;
 
-                for (Unseekable keyOrRange : value.participants.intersecting(keysOrRanges, Minimal))
+                Unseekables<?> participants = value.participants;
+                if (keysOrRanges != null)
+                    participants = participants.intersecting(keysOrRanges, Minimal);
+
+                for (Unseekable keyOrRange : participants)
                     visit.visit(p1, p2, value.status(), value.durability(), keyOrRange, value.plainTxnId());
             }
         }
diff --git a/accord-core/src/main/java/accord/local/DepsCalculator.java b/accord-core/src/main/java/accord/local/DepsCalculator.java
index 5cfc4db1..6391854e 100644
--- a/accord-core/src/main/java/accord/local/DepsCalculator.java
+++ b/accord-core/src/main/java/accord/local/DepsCalculator.java
@@ -18,11 +18,16 @@
 
 package accord.local;
 
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import javax.annotation.Nullable;
 
 import accord.coordinate.ExecuteFlag.ExecuteFlags;
 import accord.local.CommandSummaries.SummaryStatus;
 import accord.local.MaxDecidedRX.DecidedRX;
+import accord.messages.Reply;
+import accord.messages.ReplyList;
 import accord.primitives.Deps;
 import accord.primitives.EpochSupplier;
 import accord.primitives.Participants;
@@ -33,14 +38,25 @@ import accord.primitives.TxnId;
 import accord.primitives.Unseekable;
 import accord.primitives.Unseekables;
 import accord.utils.Invariants;
+import accord.utils.UnhandledEnum;
+import accord.utils.async.AsyncChain;
+import accord.utils.async.AsyncResult;
+import accord.utils.async.AsyncResults;
+import accord.utils.async.AsyncResults.AbstractImmediate;
+import accord.utils.async.CancellableAsyncResult;
 
 import static accord.coordinate.ExecuteFlag.HAS_UNIQUE_HLC;
 import static accord.coordinate.ExecuteFlag.READY_TO_EXECUTE;
 import static accord.local.CommandSummaries.SummaryStatus.APPLIED;
+import static accord.local.DepsCalculator.Initialised.DONE;
+import static accord.local.DepsCalculator.Initialised.INCOMPLETE;
+import static accord.local.DepsCalculator.Initialised.REJECTED;
+import static accord.local.LoadKeys.INCR;
 import static accord.primitives.Txn.Kind.EphemeralRead;
 import static accord.primitives.Txn.Kind.ExclusiveSyncPoint;
+import static accord.utils.Invariants.illegalState;
 
-public class DepsCalculator extends Deps.Builder implements CommandSummaries.ActiveCommandVisitor<TxnId, DepsCalculator.MinDependencyCalculator>
+public abstract class DepsCalculator extends Deps.Builder implements CommandSummaries.ActiveCommandVisitor<TxnId, DepsCalculator.MinDependencyCalculator>, Consumer<SafeCommandStore>, ExecutionContext
 {
     public static class MinDependencyCalculator
     {
@@ -77,32 +93,182 @@ public class DepsCalculator extends Deps.Builder implements CommandSummaries.Act
         }
     }
 
+    public static final class SynchronousDepsCalculator extends DepsCalculator
+    {
+        public SynchronousDepsCalculator(TxnId txnId, Timestamp executeAt, Participants<?> touches)
+        {
+            super(txnId, executeAt, touches);
+        }
+
+        public Deps calculate(SafeCommandStore safeStore, long minEpoch, boolean rejectIfRedundant)
+        {
+            Initialised initialised = initialise(safeStore, minEpoch, rejectIfRedundant);
+            switch (initialised)
+            {
+                default: throw new UnhandledEnum(initialised);
+                case INCOMPLETE: throw illegalState("Could not calculate dependencies - must have declared insufficient ExecutionContext (%s vs %s)", safeStore.context(), this);
+                case REJECTED: return null;
+                case DONE: return deps();
+            }
+        }
+
+        public static Deps calculateDeps(SafeCommandStore safeStore, TxnId txnId, StoreParticipants participants, long minEpoch, Timestamp executeAt, boolean rejectIfRedundant)
+        {
+            return calculateDeps(safeStore, txnId, participants.touches(), minEpoch, executeAt, rejectIfRedundant);
+        }
+
+        public static Deps calculateDeps(SafeCommandStore safeStore, TxnId txnId, Participants<?> touches, long minEpoch, Timestamp executeAt, boolean rejectIfRedundant)
+        {
+            try (SynchronousDepsCalculator calculator = new SynchronousDepsCalculator(txnId, executeAt, touches))
+            {
+                return calculator.calculate(safeStore, minEpoch, rejectIfRedundant);
+            }
+        }
+    }
+
+    public abstract static class AbstractDepsReply<R extends AbstractDepsReply<R>> extends AbstractImmediate<R> implements ReplyList<R>, Reply, CancellableAsyncResult<R>
+    {
+        @Override
+        public boolean isSuccess()
+        {
+            return true;
+        }
+
+        @Override
+        public AsyncResult<R> invoke(BiConsumer<? super R, Throwable> callback)
+        {
+            callback.accept((R)this, null);
+            return this;
+        }
+
+        @Override
+        public int size()
+        {
+            return 1;
+        }
+
+        @Override
+        public CancellableAsyncResult<R> get(int i)
+        {
+            Invariants.requireArgument(i == 0);
+            return this;
+        }
+
+        @Override
+        public void cancel()
+        {
+        }
+
+        @Override
+        public void cancelReplies()
+        {
+        }
+    }
+
+    static class AsyncDepsReply<R extends AbstractDepsReply<R>> extends AsyncResults.CancellableChain<R> implements ReplyList<R>
+    {
+        public AsyncDepsReply(AsyncChain<R> chain)
+        {
+            super(chain);
+        }
+
+        @Override
+        public int size()
+        {
+            return 1;
+        }
+
+        @Override
+        public CancellableAsyncResult<R> get(int i)
+        {
+            Invariants.require(i == 0);
+            return this;
+        }
+
+        @Override
+        public void cancelReplies()
+        {
+            cancel();
+        }
+    }
+
+    public abstract static class DepsReplyCalculator<R extends AbstractDepsReply<R>> extends DepsCalculator implements Function<Void, R>
+    {
+        public DepsReplyCalculator(TxnId txnId, Timestamp executeAt, StoreParticipants participants)
+        {
+            super(txnId, executeAt, participants);
+        }
+
+        public DepsReplyCalculator(TxnId txnId, Timestamp executeAt, Participants<?> touches)
+        {
+            super(txnId, executeAt, touches);
+        }
+
+        public ReplyList<R> calculate(SafeCommandStore safeStore, long minEpoch, boolean nullIfRedundant)
+        {
+            Initialised initialised;
+            try
+            {
+                initialised = initialise(safeStore, minEpoch, nullIfRedundant);
+            }
+            catch (Throwable t)
+            {
+                close();
+                throw t;
+            }
+
+            switch (initialised)
+            {
+                default: throw new UnhandledEnum(initialised);
+                case REJECTED:
+                    return null;
+                case DONE:
+                    return apply(null);
+                case INCOMPLETE:
+                    AsyncChain<R> chain = safeStore.commandStore().continuationChain(this, this).map(this);
+                    return new AsyncDepsReply<>(chain);
+            }
+        }
+    }
+
     // TODO (expected): we can also track whether we have only single-key writes that have been Accepted with ballot 0 (or timestamp != t0), or else Committed[1];
     //  in this case we can decide immediately if we have a unique hlc as we don't run the risk of other keys inserting some arbitrary timestamp
     //  [1] probably unsafe to use Accepted with ballot > 0, as there could be a timestamp battle, and the timestamp we see might not be the one that gets decided.
-    private final long now;
+    protected final TxnId txnId;
+    protected final Timestamp executeAt;
+    private final Participants<?> touches;
+    private RangeDeps redundant;
     private long sumUnappliedAge, maxUnappliedAge;
     private int unappliedCount;
     private long maxAppliedHlc;
+    private MinDependencyCalculator minDepCalc;
 
-    public DepsCalculator(Timestamp timestamp)
+    public DepsCalculator(TxnId txnId, Timestamp executeAt, StoreParticipants touches)
+    {
+        this(txnId, executeAt, touches.touches());
+    }
+
+    public DepsCalculator(TxnId txnId, Timestamp executeAt, Participants<?> touches)
     {
         super(true);
-        this.now = timestamp.hlc();
+        this.txnId = txnId;
+        this.touches = touches;
+        this.executeAt = executeAt.equals(txnId) ? txnId : executeAt;
     }
 
     @Override
-    public void visit(TxnId self, @Nullable MinDependencyCalculator minDepCalc, SummaryStatus status, Durability durability, Unseekable keyOrRange, TxnId depId)
+    public final void visit(TxnId self, @Nullable MinDependencyCalculator minDepCalc, SummaryStatus status, Durability durability, Unseekable keyOrRange, TxnId depId)
     {
         if (minDepCalc != null && !minDepCalc.include(durability, keyOrRange, depId))
             return;
 
         if (self == null || !self.equals(depId))
             add(keyOrRange, depId);
+
         if (status.compareTo(APPLIED) < 0)
         {
             unappliedCount += 1;
-            long age = Math.max(0, now - depId.hlc());
+            long age = Math.max(0, executeAt.hlc() - depId.hlc());
             sumUnappliedAge += age;
             if (age > maxUnappliedAge)
                 maxUnappliedAge = age;
@@ -110,13 +276,13 @@ public class DepsCalculator extends Deps.Builder implements CommandSummaries.Act
     }
 
     @Override
-    public void visitMaxAppliedHlc(long maxAppliedHlc)
+    public final void visitMaxAppliedHlc(long maxAppliedHlc)
     {
         if (maxAppliedHlc > this.maxAppliedHlc)
             this.maxAppliedHlc = maxAppliedHlc;
     }
 
-    public ExecuteFlags executeFlags(TxnId txnId)
+    public final ExecuteFlags executeFlags()
     {
         ExecuteFlags flags = ExecuteFlags.none();
         if (unappliedCount == 0)
@@ -129,59 +295,110 @@ public class DepsCalculator extends Deps.Builder implements CommandSummaries.Act
         return flags;
     }
 
-    public Timestamp executeAt(SafeCommand safeCommand, Node node)
+    public final Deps deps()
     {
-        Timestamp executeAt = safeCommand.current().executeAtOrTxnId();
+        Deps result = super.build();
+        result = new Deps(result.keyDeps, result.rangeDeps.with(redundant));
+        Invariants.require(!txnId.isVisible() || !result.contains(txnId));
+        return result;
+    }
+
+    public final Timestamp executeAt(Timestamp witnessedAt, Node node)
+    {
+        Timestamp executeAt = witnessedAt;
         if (unappliedCount > 0 && node.agent().softReject(unappliedCount, maxUnappliedAge, sumUnappliedAge))
             executeAt = executeAt.addFlag(Timestamp.Flag.SOFT_REJECT);
         return executeAt;
     }
 
-    public Deps calculate(SafeCommandStore safeStore, TxnId txnId, StoreParticipants participants, long minEpoch, Timestamp executeAt, boolean nullIfRedundant)
+    public enum Initialised
     {
-        return calculate(safeStore, txnId, participants.touches(), minEpoch, executeAt, nullIfRedundant);
+        REJECTED, DONE, INCOMPLETE
     }
 
-    public Deps calculate(SafeCommandStore safeStore, TxnId txnId, Participants<?> touches, long minEpoch, Timestamp executeAt, boolean nullIfRedundant)
+    public final Initialised initialise(SafeCommandStore safeStore, long minEpoch, boolean rejectIfRedundant)
     {
-        RangeDeps redundant;
         try (RangeDeps.BuilderByRange redundantBuilder = RangeDeps.builderByRange())
         {
             redundant = safeStore.redundantBefore().collectDeps(touches, redundantBuilder, EpochSupplier.constant(minEpoch), executeAt)
                                  .build();
         }
 
-        if (nullIfRedundant && !txnId.is(EphemeralRead))
+        if (rejectIfRedundant && !txnId.is(EphemeralRead))
         {
             TxnId maxRedundantBefore = redundant.maxTxnId(null);
             if (maxRedundantBefore != null && maxRedundantBefore.compareTo(executeAt) >= 0)
             {
                 Invariants.require(maxRedundantBefore.isSyncPoint());
-                return null;
+                return REJECTED;
             }
         }
 
-        // NOTE: ExclusiveSyncPoint *relies* on STARTED_BEFORE to ensure it reports a dependency on *every* earlier TxnId that may execute (before or after it).
-        MinDependencyCalculator minDepCalc = null;
         // the main difference between RX and RV is whether we apply this filtering
-        if (txnId.is(ExclusiveSyncPoint)) minDepCalc = new MinDependencyCalculator(safeStore.maxDecidedRX(), touches, txnId);
-        safeStore.visit(touches, executeAt, txnId.witnesses(), this, executeAt.equals(txnId) ? null : txnId, minDepCalc);
-        Deps result = super.build();
-        result = new Deps(result.keyDeps, result.rangeDeps.with(redundant));
-        Invariants.require(!txnId.isVisible() || !result.contains(txnId));
-        return result;
-    }
+        if (txnId.is(ExclusiveSyncPoint))
+            minDepCalc = new MinDependencyCalculator(safeStore.maxDecidedRX(), touches, txnId);
 
-    public static Deps calculateDeps(SafeCommandStore safeStore, TxnId txnId, StoreParticipants participants, long minEpoch, Timestamp executeAt, boolean nullIfRedundant)
-    {
-        return calculateDeps(safeStore, txnId, participants.touches(), minEpoch, executeAt, nullIfRedundant);
+        safeStore.visit(touches, executeAt, txnId.witnesses(), this, executeAt == txnId ? null : txnId, minDepCalc);
+        if (safeStore.context().keys().containsAll(touches))
+            return DONE;
+
+        return INCOMPLETE;
     }
 
-    public static Deps calculateDeps(SafeCommandStore safeStore, TxnId txnId, Participants<?> touches, long minEpoch, Timestamp executeAt, boolean nullIfRedundant)
+    @Override
+    public void accept(SafeCommandStore safeStore)
     {
-        try (DepsCalculator calculator = new DepsCalculator(executeAt))
+        try
         {
-            return calculator.calculate(safeStore, txnId, touches, minEpoch, executeAt, nullIfRedundant);
+            safeStore.visit(null, executeAt, txnId.witnesses(), this, executeAt == txnId ? null : txnId, minDepCalc);
         }
+        catch (Throwable t)
+        {
+            try { close(); }
+            catch (Throwable t2) { try { t.addSuppressed(t2); } catch (Throwable ignore) {} }
+            throw t;
+        }
+    }
+
+    @Override
+    public TxnId primaryTxnId()
+    {
+        return txnId;
+    }
+
+    @Override
+    public String reason()
+    {
+        return "Calculate Deps";
+    }
+
+    @Override
+    public boolean abandonPartialSuccess()
+    {
+        return true;
+    }
+
+    @Override
+    public Unseekables<?> keys()
+    {
+        return touches;
+    }
+
+    @Override
+    public LoadKeys loadKeys()
+    {
+        return INCR;
+    }
+
+    @Override
+    public FindKeys findKeys()
+    {
+        return FindKeys.CONFLICTS;
+    }
+
+    @Override
+    public ExecutionSequence executionSequence()
+    {
+        return ExecutionSequence.ATOMIC;
     }
 }
diff --git a/accord-core/src/main/java/accord/local/ExecutionContext.java b/accord-core/src/main/java/accord/local/ExecutionContext.java
index 54a500ff..4c51968b 100644
--- a/accord-core/src/main/java/accord/local/ExecutionContext.java
+++ b/accord-core/src/main/java/accord/local/ExecutionContext.java
@@ -18,33 +18,31 @@
 
 package accord.local;
 
-import accord.local.cfk.CommandsForKey;
+import java.util.AbstractList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javax.annotation.Nullable;
+
+import net.nicoulaj.compilecommand.annotations.Inline;
+
 import accord.primitives.Ranges;
 import accord.primitives.Routables.Slice;
 import accord.primitives.RoutingKeys;
 import accord.primitives.Timestamp;
 import accord.primitives.TxnId;
-
 import accord.primitives.Unseekables;
 import accord.utils.Invariants;
-import net.nicoulaj.compilecommand.annotations.Inline;
-
-import java.util.AbstractList;
-import java.util.List;
-import java.util.function.Consumer;
-import javax.annotation.Nullable;
 
 import static accord.local.LoadKeys.INCR;
 import static accord.local.LoadKeys.NONE;
 import static accord.local.LoadKeys.SYNC;
-import static accord.local.LoadKeysFor.READ_WRITE;
-import static accord.local.LoadKeysFor.WRITE;
+import static accord.local.FindKeys.CONFLICTS;
+import static accord.local.FindKeys.DECLARED;
 
 /**
- * Lists txnids and keys of commands and commands for key that will be needed for an operation. Used
- * to ensure the necessary state is in memory for an operation before it executes.
- *
- * TODO (desired): rename to simply Context, or LoadContext
+ * Tasks declare required data and semantics for their execution.
+ * An INCR or ASYNC execution
  */
 public interface ExecutionContext
 {
@@ -144,13 +142,13 @@ public interface ExecutionContext
     }
 
     /**
-     * @return keys of the {@link CommandsForKey} objects that need to be loaded into memory before this operation is run
+     * @return keys or ranges that this key needs CommandSummaries loaded for
      */
     default Unseekables<?> keys() { return RoutingKeys.EMPTY; }
 
     default LoadKeys loadKeys() { return NONE; }
 
-    default LoadKeysFor loadKeysFor() { return WRITE; }
+    default FindKeys findKeys() { return DECLARED; }
 
     /**
      * Whether this execution may be retried safely; useful only for INCR tasks that may partially succeed,
@@ -158,6 +156,11 @@ public interface ExecutionContext
      */
     default boolean isIdempotent() { return false; }
 
+    /**
+     * Whether this execution should be retried if partially executes; useful only for INCR tasks that may partially succeed.
+     */
+    default boolean abandonPartialSuccess() { return false; }
+
     default ExecutionKind executionKind() { return ExecutionKind.OTHER; }
 
     default ExecutionSequence executionSequence() { return ExecutionSequence.BY_PRIORITY; }
@@ -189,7 +192,7 @@ public interface ExecutionContext
                     requiredHistory = SYNC;
                 if (requiredHistory.compareTo(superset.loadKeys()) < 0)
                     return false;
-                if (loadKeysFor().compareTo(superset.loadKeysFor()) > 0)
+                if (findKeys().compareTo(superset.findKeys()) > 0)
                     return false;
             }
 
@@ -229,8 +232,9 @@ public interface ExecutionContext
         @Override default ExecutionSequence executionSequence() { return wrapped().executionSequence(); }
         @Override default ExecutionKind executionKind() { return wrapped().executionKind(); }
         @Override default boolean isIdempotent() { return wrapped().isIdempotent(); }
+        @Override default boolean abandonPartialSuccess() { return wrapped().abandonPartialSuccess(); }
         @Override default LoadKeys loadKeys() { return wrapped().loadKeys(); }
-        @Override default LoadKeysFor loadKeysFor() { return wrapped().loadKeysFor(); }
+        @Override default FindKeys findKeys() { return wrapped().findKeys(); }
         @Override default Timestamp executeAt() { return wrapped().executeAt(); }
         @Override default String reason() { return wrapped().reason(); }
         @Override default String describe() { return wrapped().describe(); }
@@ -251,7 +255,7 @@ public interface ExecutionContext
         @Override public ExecutionContext wrapped() { return wrapped; }
     }
 
-    static ExecutionContext contextFor(@Nullable TxnId primary, @Nullable TxnId additional, Unseekables<?> keys, LoadKeys loadKeys, LoadKeysFor loadKeysFor, String reason)
+    static ExecutionContext contextFor(@Nullable TxnId primary, @Nullable TxnId additional, Unseekables<?> keys, LoadKeys loadKeys, FindKeys findKeys, String reason)
     {
         Invariants.require(primary == null ? additional == null : !primary.equals(additional));
         return new ExecutionContext()
@@ -260,7 +264,7 @@ public interface ExecutionContext
             @Override public @Nullable TxnId additionalTxnId() { return additional; }
             @Override public Unseekables<?> keys() { return keys; }
             @Override public LoadKeys loadKeys() { return loadKeys; }
-            @Override public LoadKeysFor loadKeysFor() { return loadKeysFor; }
+            @Override public FindKeys findKeys() { return findKeys; }
             @Override public String reason() { return reason; }
             @Override public String toString() { return describe(); }
         };
@@ -328,7 +332,7 @@ public interface ExecutionContext
             @Override public @Nullable TxnId primaryTxnId() { return txnId; }
             @Override public Unseekables<?> keys() { return keys; }
             @Override public LoadKeys loadKeys() { return SYNC; }
-            @Override public LoadKeysFor loadKeysFor() { return READ_WRITE; }
+            @Override public FindKeys findKeys() { return CONFLICTS; }
             @Override public ExecutionSequence executionSequence() { return ExecutionSequence.UNSEQUENCED; }
             @Override public String reason() { return reason; }
             @Override public String toString() { return describe(); }
@@ -342,7 +346,7 @@ public interface ExecutionContext
             @Override public @Nullable TxnId primaryTxnId() { return null; }
             @Override public Unseekables<?> keys() { return keys; }
             @Override public LoadKeys loadKeys() { return SYNC; }
-            @Override public LoadKeysFor loadKeysFor() { return READ_WRITE; }
+            @Override public FindKeys findKeys() { return CONFLICTS; }
             @Override public ExecutionSequence executionSequence() { return ExecutionSequence.UNSEQUENCED; }
             @Override public String reason() { return reason; }
             @Override public String toString() { return describe(); }
diff --git a/accord-core/src/main/java/accord/local/LoadKeysFor.java b/accord-core/src/main/java/accord/local/FindKeys.java
similarity index 78%
rename from accord-core/src/main/java/accord/local/LoadKeysFor.java
rename to accord-core/src/main/java/accord/local/FindKeys.java
index dcf14f12..6ee0454c 100644
--- a/accord-core/src/main/java/accord/local/LoadKeysFor.java
+++ b/accord-core/src/main/java/accord/local/FindKeys.java
@@ -20,29 +20,29 @@ package accord.local;
 
 /**
  * For operations that need information associated with keys or ranges,
- * this indicates whether the data will be queried, updated, or both.
+ * this indicates what data will be queried.
  */
-public enum LoadKeysFor
+public enum FindKeys
 {
     /**
-     * WRITE covers only updating the relevant key state for the primaryTxnId,
+     * covers only updating the relevant key state for the primaryTxnId,
      * that is for key transactions this means updating key summaries, and for
      * range transactions this means updating any range summaries.
      * Importantly, this does not mean range transactions must be able to
      * synchronously (or otherwise) write to all intersecting key summaries.
      */
-    WRITE,
+    DECLARED,
 
     /**
-     * READ covers all intersecting summaries of relevant key or range transactions,
+     * CONFLICTS covers all intersecting summaries of relevant key or range transactions,
      * including any commands that should be witnessed by primaryTxnId.
      * This means range transactions MUST be able to consult all intersecting key summaries.
      */
-    READ_WRITE,
+    CONFLICTS,
 
     /**
-     * RECOVERY is READ_WRITE + summary information of keys/transactions that should have witnessed
+     * SUPERSEDING is CONFLICTS + summary information of keys/transactions that should have witnessed
      * the primaryTxnId.
      */
-    RECOVERY
+    SUPERSEDING
 }
diff --git a/accord-core/src/main/java/accord/local/LoadKeys.java b/accord-core/src/main/java/accord/local/LoadKeys.java
index 8de1d782..48549df3 100644
--- a/accord-core/src/main/java/accord/local/LoadKeys.java
+++ b/accord-core/src/main/java/accord/local/LoadKeys.java
@@ -35,10 +35,15 @@ public enum LoadKeys
     ASYNC,
 
     /**
-     * Load and process the requested keys incrementally; the operation will be invoked multiples times
-     * as keys are loaded, until all the keys have been processed. If submitted by an already running execution
-     * this task must declare a subset of the keys and txnIds declared by the originating task.
-     * It is not permitted to chain INCR tasks together; INCR may only be submitted by an ASYNC or SYNC task.
+     * Important Notes:
+     *  1) An INCR task only adopts command summaries (keys or ranges) that were not loaded by its parent task, so the
+     *     parent task MUST process any keys that are available to it.
+     *  2) {@link SafeCommandStore#context()} will report only the KEYS that have been loaded for a run, but if there
+     *     are range summaries to process then the first invocation will include these as well. To visit all loaded
+     *     summaries ensure to pass {@code null} to {@link SafeCommandStore#visit}.
+     *<p>
+     * Load and process the requested key and range command summaries incrementally; the operation will be invoked
+     * multiples times as keys are loaded, until all the keys have been processed.
      */
     INCR,
 
@@ -70,5 +75,4 @@ public enum LoadKeys
                 return this.compareTo(ifSyncRequireAtLeast) >= 0;
         }
     }
-
 }
diff --git a/accord-core/src/main/java/accord/local/MapReduceCommandStores.java b/accord-core/src/main/java/accord/local/MapReduceCommandStores.java
index 008cd02e..5ca2ee11 100644
--- a/accord-core/src/main/java/accord/local/MapReduceCommandStores.java
+++ b/accord-core/src/main/java/accord/local/MapReduceCommandStores.java
@@ -60,6 +60,8 @@ public abstract class MapReduceCommandStores<P extends Participants<?>, O> imple
 
     protected AsyncChain<O> applyAsyncInternal(Ranges ranges, CommandStore commandStore)
     {
+        // TODO (desired): shouldn't need to override the context to supply the ranges we interact with
+        //   (perhaps accept Ranges as another method signature, or else leave to commandStore to figure out)
         return commandStore.chain(slice(ranges, Minimal), this);
     }
 
diff --git a/accord-core/src/main/java/accord/local/SafeCommandStore.java b/accord-core/src/main/java/accord/local/SafeCommandStore.java
index 0a9c2051..85d268f1 100644
--- a/accord-core/src/main/java/accord/local/SafeCommandStore.java
+++ b/accord-core/src/main/java/accord/local/SafeCommandStore.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.NavigableMap;
 import java.util.function.Consumer;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -53,8 +54,8 @@ import accord.primitives.Timestamp;
 import accord.primitives.TxnId;
 import accord.primitives.Unseekables;
 import accord.utils.Invariants;
-import accord.utils.Reduce;
 import accord.utils.LargeBitSet;
+import accord.utils.Reduce;
 import accord.utils.SortedList;
 import accord.utils.async.AsyncChain;
 import accord.utils.async.AsyncChains;
@@ -63,9 +64,9 @@ import static accord.local.ExecutionContext.unsequencedIdempotentIncrementalWrit
 import static accord.local.LoadKeys.INCR;
 import static accord.local.LoadKeys.NONE;
 import static accord.local.RedundantStatus.Property.LOCALLY_APPLIED;
-import static accord.local.RedundantStatus.SomeStatus.LOCALLY_WITNESSED_ONLY;
 import static accord.local.RedundantStatus.Property.LOCALLY_REDUNDANT;
 import static accord.local.RedundantStatus.Property.SHARD_APPLIED;
+import static accord.local.RedundantStatus.SomeStatus.LOCALLY_WITNESSED_ONLY;
 import static accord.local.cfk.UpdateUnmanagedMode.REGISTER;
 import static accord.primitives.Known.KnownRoute.MaybeRoute;
 import static accord.primitives.Routable.Domain.Range;
@@ -293,7 +294,7 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
     public final boolean canExecuteWith(ExecutionContext context) { return canExecute(context) == context; }
 
     /**
-     * Attempt to ready the provided PreLoadContext; if this can only be achieved partially, a new PreLoadContext
+     * Attempt to ready the provided PreLoadContext; if this can only be achieved partially, a new ExecutionContext
      * will be returned containing the readily available data. If nothing is available, null will be returned.
      */
     public abstract @Nullable ExecutionContext canExecute(ExecutionContext context);
@@ -413,7 +414,7 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
             return;
 
         // TODO (expected): we don't want to insert any dependencies for those we only touch; we just need to record them as decided/applied for execution
-        ExecutionContext context = new UpdateManagedContext(next.txnId(), update);
+        ExecutionContext context = new UpdateManagedContext(next.txnId, update);
         ExecutionContext execute = safeStore.canExecute(context);
         if (execute != null)
         {
@@ -431,7 +432,7 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
                 Unseekables<?> asyncKeys = remainingKeys.without(participants.touches()).intersecting(participants.hasTouched(), Minimal);
                 if (!asyncKeys.isEmpty())
                 {
-                    ExecutionContext async = unsequencedIdempotentIncrementalWrite(asyncKeys, "Update CommandsForKey");
+                    ExecutionContext async = new UpdateManagedContext(next.txnId, asyncKeys);
                     updateManagedCommandsForKeyIncremental(async, safeStore.commandStore(), forceNotify);
                     remainingKeys = remainingKeys.without(asyncKeys);
                 }
@@ -528,6 +529,8 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
         }
         else
         {
+            // TODO (expected): no need to filter keys, as INCR tasks should subtract parent keys
+            //    (but should first synchronise executor behaviour between accord/C*)
             if (execute != null)
                 context = new UpdateUnmanagedContext(txnId, keys.without(execute.keys()));
 
@@ -540,7 +543,7 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
         }
     }
 
-    static class UpdateManagedContext implements ExecutionContext
+    static final class UpdateManagedContext implements ExecutionContext
     {
         final TxnId primaryTxnId;
         final Unseekables<?> keys;
@@ -560,7 +563,7 @@ public abstract class SafeCommandStore implements RangesForEpochSupplier, Redund
         @Override public String toString() { return describe(); }
     }
 
-    static class UpdateUnmanagedContext implements ExecutionContext
+    static final class UpdateUnmanagedContext implements ExecutionContext
     {
         final TxnId primaryTxnId;
         final Unseekables<?> keys;
diff --git a/accord-core/src/main/java/accord/local/durability/DurabilityLevel.java b/accord-core/src/main/java/accord/local/durability/DurabilityLevel.java
index 8e79f29b..051ec0ed 100644
--- a/accord-core/src/main/java/accord/local/durability/DurabilityLevel.java
+++ b/accord-core/src/main/java/accord/local/durability/DurabilityLevel.java
@@ -79,8 +79,8 @@ public class DurabilityLevel
     {
         SyncLocal local = min(a.local, b.local);
         SyncRemote remote = min(a.remote, b.remote);
-        SortedArrayList<Node.Id> including = union(a.including, b.including);
         SortedArrayList<Node.Id> excluding = union(a.excluding, b.excluding);
+        SortedArrayList<Node.Id> including = subtract(union(a.including, b.including), excluding);
         if (including != null && excluding != null)
             including = including.without(excluding);
         return new DurabilityLevel(local, remote, including, excluding);
@@ -91,7 +91,7 @@ public class DurabilityLevel
         SyncLocal local = max(a.local, b.local);
         SyncRemote remote = max(a.remote, b.remote);
         SortedArrayList<Node.Id> including = union(a.including, b.including);
-        SortedArrayList<Node.Id> excluding = subtract(a.excluding, b.excluding);
+        SortedArrayList<Node.Id> excluding = subtract(union(a.excluding, b.excluding), including);
         if (including != null && excluding != null)
             including = including.without(excluding);
         return new DurabilityLevel(local, remote, including, excluding);
diff --git a/accord-core/src/main/java/accord/messages/Accept.java b/accord-core/src/main/java/accord/messages/Accept.java
index 85186062..fff16fed 100644
--- a/accord-core/src/main/java/accord/messages/Accept.java
+++ b/accord-core/src/main/java/accord/messages/Accept.java
@@ -18,6 +18,7 @@
 
 package accord.messages;
 
+import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -25,9 +26,10 @@ import accord.coordinate.ExecuteFlag.ExecuteFlags;
 import accord.local.Command;
 import accord.local.Commands;
 import accord.local.Commands.AcceptOutcome;
-import accord.local.DepsCalculator;
+import accord.local.DepsCalculator.AbstractDepsReply;
+import accord.local.DepsCalculator.DepsReplyCalculator;
 import accord.local.LoadKeys;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
 import accord.local.Node.Id;
 import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
@@ -49,11 +51,11 @@ import accord.utils.UnhandledEnum;
 import accord.utils.async.Cancellable;
 
 import static accord.api.ProtocolModifiers.filterDuplicateDependenciesFromAcceptReply;
+import static accord.api.ProtocolModifiers.loadKeysAsyncIfPermitted;
 import static accord.api.ProtocolModifiers.syncPointsTrackUnstableMediumPathDependencies;
 import static accord.local.Commands.AcceptOutcome.Redundant;
 import static accord.local.Commands.AcceptOutcome.RejectedBallot;
 import static accord.local.Commands.AcceptOutcome.Success;
-import static accord.local.LoadKeys.SYNC;
 import static accord.messages.MessageType.StandardMessage.ACCEPT_REQ;
 import static accord.messages.MessageType.StandardMessage.ACCEPT_RSP;
 import static accord.messages.MessageType.StandardMessage.NOT_ACCEPT_REQ;
@@ -61,7 +63,7 @@ import static accord.primitives.Known.KnownDeps.DepsKnown;
 
 // 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 RouteRequest.WithUnsynced<Accept.AcceptReply>
+public class Accept extends RouteRequest.WithUnsynced<ReplyList<Accept.AcceptReply>>
 {
     public static class SerializerSupport
     {
@@ -140,15 +142,15 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
     }
 
     @Override
-    public AcceptReply applyInternal(SafeCommandStore safeStore)
+    public ReplyList<AcceptReply> applyInternal(SafeCommandStore safeStore)
     {
-        PartialDeps partialDeps = this.partialDeps;
+        PartialDeps inputDeps = this.partialDeps;
         if (ifDoneExpectCancelled()) // check cancellation after reading nullable fields
             return null; // we can't throw an exception here else we override any non-exceptional reply informing the reason
 
         StoreParticipants participants = StoreParticipants.update(safeStore, scope, minEpoch, txnId, txnId.epoch(), executeAt.epoch());
         SafeCommand safeCommand = safeStore.get(txnId, participants);
-        AcceptOutcome outcome = Commands.accept(safeStore, safeCommand, participants, txnId, kind, ballot, scope, executeAt, partialDeps);
+        AcceptOutcome outcome = Commands.accept(safeStore, safeCommand, participants, txnId, kind, ballot, scope, executeAt, inputDeps);
         switch (outcome)
         {
             default: throw new UnhandledEnum(outcome);
@@ -159,13 +161,14 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
 
                 boolean notOwner = participants.owns().isEmpty();
                 Participants<?> hasDeps = null;
-                Deps deps = null;
+                Deps foundDeps;
 
                 if (command.known().is(DepsKnown) && (isPartialAccept() || notOwner))
                 {
-                    deps = command.partialDeps().asFullUnsafe();
+                    foundDeps = command.partialDeps().asFullUnsafe();
                     hasDeps = command.participants().stillTouches();
                 }
+                else foundDeps = null;
 
                 Ballot superseding = command.promised();
                 if (superseding.compareTo(ballot) <= 0)
@@ -179,30 +182,36 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
                     calculateDeps = calculateDeps();
                 }
 
+                final Deps removeDeps = filterDeps() ? inputDeps : null;
+                final Timestamp executeAtIfKnown = command.executeAtIfKnown();
+
                 if (calculateDeps)
                 {
                     Participants<?> calculate = participants.touches();
                     if (hasDeps != null)
                         calculate = calculate.without(hasDeps);
+                    hasDeps = participants.touches();
 
                     if (!calculate.isEmpty())
                     {
-                        Deps calculatedDeps = DepsCalculator.calculateDeps(safeStore, txnId, calculate, minEpoch, executeAt, true);
-                        if (calculatedDeps == null)
-                            return AcceptReply.inThePast(ballot, participants, command);
-
-                        deps = deps == null ? calculatedDeps : calculatedDeps.with(deps);
+                        Participants<?> successful = replySuccessful(hasDeps);
+                        outcome = replyOutcome(notOwner, outcome, participants, hasDeps);
+                        //noinspection resource
+                        TruncatedAcceptDepsCalculator calculator = new TruncatedAcceptDepsCalculator(txnId, executeAt, calculate, foundDeps, removeDeps, outcome, superseding, successful, executeAtIfKnown);
+                        ReplyList<AcceptReply> reply = calculator.calculate(safeStore, minEpoch, true);
+                        if (reply == null)
+                            reply = AcceptReply.inThePast(ballot, participants, command);
+                        return reply;
                     }
-                    hasDeps = participants.touches();
                 }
 
-                Participants<?> successful = isPartialAccept() ? hasDeps : null;
-                if (notOwner && (outcome == Redundant || (hasDeps != null && hasDeps.containsAll(participants.touches()))))
-                    outcome = Success;
+                Deps deps = foundDeps;
+                if (deps != null && removeDeps != null)
+                    deps = deps.without(removeDeps);
 
-                if (deps != null && filterDeps())
-                    deps = deps.without(partialDeps);
-                return new AcceptReply(outcome, superseding, successful, deps, command.executeAtIfKnown());
+                Participants<?> successful = replySuccessful(hasDeps);
+                outcome = replyOutcome(notOwner, outcome, participants, hasDeps);
+                return new AcceptReply(outcome, superseding, successful, deps, executeAtIfKnown);
             }
 
             case RejectedBallot:
@@ -212,33 +221,34 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
                 // if we're Retired, participants.owns() is empty, so we're just fetching deps
                 // TODO (desired): optimise deps calculation; for some keys we only need to return the last RX
             case Success:
-                ExecuteFlags flags;
-                Deps deps;
-                if (calculateDeps())
-                {
-                    try (DepsCalculator calculator = new DepsCalculator(executeAt))
-                    {
-                        deps = calculator.calculate(safeStore, txnId, participants, minEpoch, executeAt, true);
-                        if (deps == null)
-                            return AcceptReply.inThePast(ballot, participants, safeCommand.current());
-                        flags = calculator.executeFlags(txnId);
-                    }
+            {
+                Participants<?> successful = isPartialAccept() ? participants.touches() : null;
 
-                    Invariants.require(deps.maxTxnId(txnId).epoch() <= executeAt.epoch());
-                    if (filterDeps())
-                        deps = deps.without(partialDeps);
-                }
-                else
-                {
-                    flags = ExecuteFlags.none();
-                    deps = Deps.NONE;
-                }
+                if (!calculateDeps())
+                    return new AcceptReply(successful, Deps.NONE, ExecuteFlags.none());
 
-                Participants<?> successful = isPartialAccept() ? participants.touches() : null;
-                return new AcceptReply(successful, deps, flags);
+                //noinspection resource
+                NormalAcceptDepsCalculator calculator = new NormalAcceptDepsCalculator(txnId, executeAt, participants, filterDeps() ? inputDeps : null, successful);
+                ReplyList<AcceptReply> reply = calculator.calculate(safeStore, minEpoch, true);
+                if (reply == null)
+                    reply = AcceptReply.inThePast(ballot, participants, safeCommand.current());
+                return reply;
+            }
         }
     }
 
+    private Participants<?> replySuccessful(Participants<?> hasDeps)
+    {
+        return isPartialAccept() ? hasDeps : null;
+    }
+
+    private AcceptOutcome replyOutcome(boolean notOwner, AcceptOutcome outcome, StoreParticipants participants, Participants<?> hasDeps)
+    {
+        if (notOwner && (outcome == Redundant || (hasDeps != null && hasDeps.containsAll(participants.touches()))))
+            return Success;
+        return outcome;
+    }
+
     private boolean isPartialAccept()
     {
         return AcceptFlags.isPartial(acceptFlags);
@@ -255,9 +265,9 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
     }
 
     @Override
-    public AcceptReply reduce(AcceptReply r1, AcceptReply r2)
+    public ReplyList<AcceptReply> reduce(ReplyList<AcceptReply> r1, ReplyList<AcceptReply> r2)
     {
-        return AcceptReply.reduce(r1, r2);
+        return ReplyList.merge(r1, r2);
     }
 
     @Override
@@ -267,23 +277,24 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
     }
 
     @Override
-    protected void acceptInternal(AcceptReply reply, Throwable failure)
+    protected void acceptInternal(ReplyList<AcceptReply> replies, Throwable fail)
     {
         // finished processing, null out large objects
         partialDeps = null;
-        super.acceptInternal(reply, failure);
+        if (fail != null || replies == null) acceptReply(null, fail);
+        else ReplyList.invoke(replies, AcceptReply::reduce, this::acceptReply);
     }
 
     @Override
     public LoadKeys loadKeys()
     {
-        return SYNC;
+        return loadKeysAsyncIfPermitted(txnId);
     }
 
     @Override
-    public LoadKeysFor loadKeysFor()
+    public FindKeys findKeys()
     {
-        return calculateDeps() ? LoadKeysFor.READ_WRITE : LoadKeysFor.WRITE;
+        return calculateDeps() ? FindKeys.CONFLICTS : FindKeys.DECLARED;
     }
 
     public ExecutionKind executionKind()
@@ -307,7 +318,79 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
                 '}';
     }
 
-    public static final class AcceptReply implements Reply
+    static class TruncatedAcceptDepsCalculator extends DepsReplyCalculator<AcceptReply> implements Function<Void, AcceptReply>
+    {
+        final Deps foundDeps;
+        final @Nullable Deps removeDeps;
+        final AcceptOutcome outcome;
+        final Ballot superseding;
+        final Participants<?> successful;
+        final Timestamp executeAtIfKnown;
+
+        public TruncatedAcceptDepsCalculator(TxnId txnId, Timestamp executeAt, Participants<?> touches, Deps foundDeps, @Nullable Deps removeDeps, AcceptOutcome outcome, Ballot superseding, Participants<?> successful, Timestamp executeAtIfKnown)
+        {
+            super(txnId, executeAt, touches);
+            this.foundDeps = foundDeps;
+            this.removeDeps = removeDeps;
+            this.outcome = outcome;
+            this.superseding = superseding;
+            this.successful = successful;
+            this.executeAtIfKnown = executeAtIfKnown;
+        }
+
+        public AcceptReply apply(Void ignore)
+        {
+            try
+            {
+                Deps deps = deps();
+                if (foundDeps != null)
+                    deps = deps.with(foundDeps);
+
+                if (deps != null && removeDeps != null)
+                    deps = deps.without(removeDeps);
+                return new AcceptReply(outcome, superseding, successful, deps, executeAtIfKnown);
+            }
+            finally { close(); }
+        }
+    }
+
+    static class NormalAcceptDepsCalculator extends DepsReplyCalculator<AcceptReply> implements Function<Void, AcceptReply>
+    {
+        final @Nullable Deps removeDeps;
+        final @Nullable Participants<?> successful;
+
+        public NormalAcceptDepsCalculator(TxnId txnId, Timestamp executeAt, StoreParticipants participants, @Nullable Deps removeDeps, @Nullable Participants<?> successful)
+        {
+            super(txnId, executeAt, participants);
+            this.removeDeps = removeDeps;
+            this.successful = successful;
+        }
+
+        public AcceptReply apply(Void ignore)
+        {
+            return finish();
+        }
+
+        AcceptReply finish()
+        {
+            try
+            {
+                Deps deps = deps();
+                Invariants.require(deps.maxTxnId(txnId).epoch() <= executeAt.epoch());
+                if (removeDeps != null)
+                    deps = deps.without(removeDeps);
+                ExecuteFlags flags = executeFlags();
+                return new AcceptReply(successful, deps, flags);
+            }
+            finally
+            {
+                close();
+            }
+        }
+    }
+
+
+    public static final class AcceptReply extends AbstractDepsReply<AcceptReply>
     {
         public static final AcceptReply SUCCESS = new AcceptReply(Success);
 
@@ -437,9 +520,9 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
                 case Success:
                     return "AcceptOk{deps=" + deps + '}';
                 case Redundant:
-                    return "AcceptRedundant(" + supersededBy + ',' + committedExecuteAt + ")";
+                    return "AcceptRedundant(" + supersededBy + ',' + committedExecuteAt + ')';
                 case RejectedBallot:
-                    return "AcceptNack(" + supersededBy + ")";
+                    return "AcceptNack(" + supersededBy + ')';
             }
         }
     }
@@ -468,6 +551,12 @@ public class Accept extends RouteRequest.WithUnsynced<Accept.AcceptReply>
             return node.commandStores().mapReduceConsume(txnId.epoch(), txnId.epoch(), this);
         }
 
+        @Override
+        protected void acceptInternal(AcceptReply reply, Throwable failure)
+        {
+            acceptReply(reply, failure);
+        }
+
         @Override
         public AcceptReply applyInternal(SafeCommandStore safeStore)
         {
diff --git a/accord-core/src/main/java/accord/messages/Apply.java b/accord-core/src/main/java/accord/messages/Apply.java
index 4bf4dd35..15cc6f50 100644
--- a/accord-core/src/main/java/accord/messages/Apply.java
+++ b/accord-core/src/main/java/accord/messages/Apply.java
@@ -152,7 +152,7 @@ public class Apply extends RouteRequest<ApplyReply>
         deps = null;
         writes = null;
         result = null;
-        if (reply != null || failure != null) super.acceptInternal(reply, failure);
+        if (reply != null || failure != null) acceptReply(reply, failure);
         else Invariants.require(isCancelled());
     }
 
diff --git a/accord-core/src/main/java/accord/messages/ApplyThenWaitUntilApplied.java b/accord-core/src/main/java/accord/messages/ApplyThenWaitUntilApplied.java
index c85bf903..29297d7f 100644
--- a/accord-core/src/main/java/accord/messages/ApplyThenWaitUntilApplied.java
+++ b/accord-core/src/main/java/accord/messages/ApplyThenWaitUntilApplied.java
@@ -23,6 +23,7 @@ import org.slf4j.LoggerFactory;
 
 import accord.api.Result;
 import accord.api.Result.PersistableResult;
+import accord.local.LoadKeys;
 import accord.local.Node;
 import accord.local.SafeCommandStore;
 import accord.local.StoreParticipants;
@@ -41,6 +42,7 @@ import accord.primitives.Writes;
 import accord.topology.Topologies;
 import accord.utils.UnhandledEnum;
 
+import static accord.api.ProtocolModifiers.loadKeysAsyncIfPermitted;
 import static accord.messages.MessageType.StandardMessage.APPLY_THEN_WAIT_UNTIL_APPLIED_REQ;
 import static accord.messages.RouteRequest.computeScope;
 
@@ -147,6 +149,12 @@ public class ApplyThenWaitUntilApplied extends WaitUntilApplied
         result = null;
     }
 
+    @Override
+    public LoadKeys loadKeys()
+    {
+        return loadKeysAsyncIfPermitted(txnId);
+    }
+
     @Override
     public MessageType type()
     {
diff --git a/accord-core/src/main/java/accord/messages/BeginInvalidation.java b/accord-core/src/main/java/accord/messages/BeginInvalidation.java
index fb0f141e..32175cc4 100644
--- a/accord-core/src/main/java/accord/messages/BeginInvalidation.java
+++ b/accord-core/src/main/java/accord/messages/BeginInvalidation.java
@@ -59,6 +59,12 @@ public class BeginInvalidation extends ParticipantsRequest<Participants<?>, Begi
         return node.commandStores().mapReduceConsume(txnId.epoch(), txnId.epoch(), this);
     }
 
+    @Override
+    protected void acceptInternal(InvalidateReply reply, Throwable failure)
+    {
+        acceptReply(reply, failure);
+    }
+
     @Override
     public InvalidateReply applyInternal(SafeCommandStore safeStore)
     {
diff --git a/accord-core/src/main/java/accord/messages/BeginRecovery.java b/accord-core/src/main/java/accord/messages/BeginRecovery.java
index 65c2da32..2321d7ea 100644
--- a/accord-core/src/main/java/accord/messages/BeginRecovery.java
+++ b/accord-core/src/main/java/accord/messages/BeginRecovery.java
@@ -19,13 +19,22 @@
 package accord.messages;
 
 import java.util.Collection;
+
 import javax.annotation.Nullable;
 
 import accord.api.Result;
-import accord.local.*;
-import accord.local.Node.Id;
+import accord.local.Command;
+import accord.local.CommandSummaries;
 import accord.local.CommandSummaries.IsDep;
 import accord.local.CommandSummaries.SummaryStatus;
+import accord.local.Commands;
+import accord.local.DepsCalculator.SynchronousDepsCalculator;
+import accord.local.LoadKeys;
+import accord.local.FindKeys;
+import accord.local.Node.Id;
+import accord.local.SafeCommand;
+import accord.local.SafeCommandStore;
+import accord.local.StoreParticipants;
 import accord.primitives.Ballot;
 import accord.primitives.Deps;
 import accord.primitives.FullRoute;
@@ -48,9 +57,9 @@ import accord.utils.TinyEnumSet;
 import accord.utils.UnhandledEnum;
 import accord.utils.async.Cancellable;
 
+import static accord.local.CommandSummaries.SummaryStatus.ACCEPTED;
 import static accord.local.CommandSummaries.SummaryStatus.APPLIED;
 import static accord.local.CommandSummaries.SummaryStatus.NOT_DIRECTLY_WITNESSED;
-import static accord.local.CommandSummaries.SummaryStatus.ACCEPTED;
 import static accord.local.CommandSummaries.SummaryStatus.STABLE;
 import static accord.messages.BeginRecovery.RecoverReply.Kind.Ok;
 import static accord.messages.BeginRecovery.RecoverReply.Kind.Reject;
@@ -133,6 +142,12 @@ public class BeginRecovery extends RouteRequest.WithUnsynced<BeginRecovery.Recov
         return node.commandStores().mapReduceConsume(minEpoch, executeAtOrTxnIdEpoch, this);
     }
 
+    @Override
+    protected void acceptInternal(RecoverReply reply, Throwable failure)
+    {
+        acceptReply(reply, failure);
+    }
+
     @Override
     public RecoverReply applyInternal(SafeCommandStore safeStore)
     {
@@ -156,7 +171,7 @@ public class BeginRecovery extends RouteRequest.WithUnsynced<BeginRecovery.Recov
             Deps localDeps = null;
             if (!command.known().deps().hasCommittedOrDecidedDeps() && calculateDeps())
             {
-                localDeps = DepsCalculator.calculateDeps(safeStore, txnId, participants, minEpoch, txnId, false);
+                localDeps = SynchronousDepsCalculator.calculateDeps(safeStore, txnId, participants, minEpoch, txnId, false);
             }
             if (localDeps != null && coordinatedDeps != null && !participants.touches().equals(coordinatedDeps.covering))
             {
@@ -273,13 +288,13 @@ public class BeginRecovery extends RouteRequest.WithUnsynced<BeginRecovery.Recov
     }
 
     @Override
-    public LoadKeysFor loadKeysFor()
+    public FindKeys findKeys()
     {
         if (recoverFastPath())
-            return LoadKeysFor.RECOVERY;
+            return FindKeys.SUPERSEDING;
         if (calculateDeps())
-            return LoadKeysFor.READ_WRITE;
-        return LoadKeysFor.WRITE;
+            return FindKeys.CONFLICTS;
+        return FindKeys.DECLARED;
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/messages/GetEphemeralReadDeps.java b/accord-core/src/main/java/accord/messages/GetEphemeralReadDeps.java
index a3b7caf4..0a7ff975 100644
--- a/accord-core/src/main/java/accord/messages/GetEphemeralReadDeps.java
+++ b/accord-core/src/main/java/accord/messages/GetEphemeralReadDeps.java
@@ -18,13 +18,16 @@
 
 package accord.messages;
 
+import java.util.function.Function;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import accord.coordinate.ExecuteFlag.ExecuteFlags;
 import accord.local.DepsCalculator;
+import accord.local.DepsCalculator.AbstractDepsReply;
 import accord.local.LoadKeys;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
 import accord.local.Node.Id;
 import accord.local.SafeCommandStore;
 import accord.local.StoreParticipants;
@@ -37,7 +40,9 @@ import accord.topology.Topologies;
 import accord.utils.Invariants;
 import accord.utils.async.Cancellable;
 
-public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<GetEphemeralReadDeps.GetEphemeralReadDepsOk>
+import static accord.api.ProtocolModifiers.loadKeysAsyncIfPermitted;
+
+public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<ReplyList<GetEphemeralReadDeps.GetEphemeralReadDepsOk>>
 {
     public static final class SerializationSupport
     {
@@ -68,7 +73,14 @@ public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<GetEphemeral
     }
 
     @Override
-    public GetEphemeralReadDepsOk applyInternal(SafeCommandStore safeStore)
+    protected void acceptInternal(ReplyList<GetEphemeralReadDepsOk> replies, Throwable failure)
+    {
+        if (failure != null) acceptReply(null, failure);
+        else ReplyList.invoke(replies, GetEphemeralReadDepsOk::reduce, this::acceptReply);
+    }
+
+    @Override
+    public ReplyList<GetEphemeralReadDepsOk> applyInternal(SafeCommandStore safeStore)
     {
         long latestEpoch = Math.max(safeStore.node().epoch(), node.epoch());
 
@@ -77,23 +89,41 @@ public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<GetEphemeral
         if (latestEpoch > executionEpoch && (safeStore.ranges().removed(executionEpoch, latestEpoch).intersects(participants.owns()) || node.topology().active().hasReplicationMaybeChanged(participants.owns(), executionEpoch)))
             return new GetEphemeralReadDepsOk(latestEpoch);
 
-        Deps deps;
-        ExecuteFlags flags;
-        try (DepsCalculator calculator = new DepsCalculator(txnId))
+        //noinspection resource
+        GetEphemeralReadDepsCalculator calculator = new GetEphemeralReadDepsCalculator(txnId, participants, latestEpoch);
+        return calculator.calculate(safeStore, minEpoch, false);
+    }
+
+    static class GetEphemeralReadDepsCalculator extends DepsCalculator.DepsReplyCalculator<GetEphemeralReadDepsOk> implements Function<Void, GetEphemeralReadDepsOk>
+    {
+        final long latestEpoch;
+
+        public GetEphemeralReadDepsCalculator(TxnId txnId, StoreParticipants participants, long latestEpoch)
         {
-             deps = calculator.calculate(safeStore, txnId, participants, minEpoch, Timestamp.MAX, false);
-             flags = calculator.executeFlags(txnId);
+            super(txnId, Timestamp.MAX, participants);
+            this.latestEpoch = latestEpoch;
+        }
+
+        public GetEphemeralReadDepsOk apply(Void ignore)
+        {
+            try
+            {
+                Deps deps = deps();
+                ExecuteFlags flags = executeFlags();
+                return new GetEphemeralReadDepsOk(deps, latestEpoch, flags);
+            }
+            finally
+            {
+                close();
+            }
         }
-        return new GetEphemeralReadDepsOk(deps, latestEpoch, flags);
     }
 
+
     @Override
-    public GetEphemeralReadDepsOk reduce(GetEphemeralReadDepsOk r1, GetEphemeralReadDepsOk r2)
+    public ReplyList<GetEphemeralReadDepsOk> reduce(ReplyList<GetEphemeralReadDepsOk> r1, ReplyList<GetEphemeralReadDepsOk> r2)
     {
-        long latestEpoch = Math.max(r1.latestEpoch, r2.latestEpoch);
-        if (r1.deps == null || r2.deps == null)
-            return new GetEphemeralReadDepsOk(latestEpoch);
-        return new GetEphemeralReadDepsOk(r1.deps.with(r2.deps), latestEpoch, r1.flags.and(r2.flags));
+        return ReplyList.merge(r1, r2);
     }
 
     @Override
@@ -114,16 +144,16 @@ public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<GetEphemeral
     @Override
     public LoadKeys loadKeys()
     {
-        return LoadKeys.SYNC;
+        return loadKeysAsyncIfPermitted(txnId);
     }
 
     @Override
-    public LoadKeysFor loadKeysFor()
+    public FindKeys findKeys()
     {
-        return LoadKeysFor.READ_WRITE;
+        return FindKeys.CONFLICTS;
     }
 
-    public static class GetEphemeralReadDepsOk implements Reply
+    public static class GetEphemeralReadDepsOk extends AbstractDepsReply<GetEphemeralReadDepsOk>
     {
         public enum Flag { READY_TO_EXECUTE }
 
@@ -156,6 +186,14 @@ public class GetEphemeralReadDeps extends RouteRequest.WithUnsynced<GetEphemeral
         {
             return MessageType.StandardMessage.GET_EPHEMERAL_READ_DEPS_RSP;
         }
+
+        private static GetEphemeralReadDepsOk reduce(GetEphemeralReadDepsOk r1, GetEphemeralReadDepsOk r2)
+        {
+            long latestEpoch = Math.max(r1.latestEpoch, r2.latestEpoch);
+            if (r1.deps == null || r2.deps == null)
+                return new GetEphemeralReadDepsOk(latestEpoch);
+            return new GetEphemeralReadDepsOk(r1.deps.with(r2.deps), latestEpoch, r1.flags.and(r2.flags));
+        }
     }
 }
 
diff --git a/accord-core/src/main/java/accord/messages/GetLatestDeps.java b/accord-core/src/main/java/accord/messages/GetLatestDeps.java
index e9993e50..42790988 100644
--- a/accord-core/src/main/java/accord/messages/GetLatestDeps.java
+++ b/accord-core/src/main/java/accord/messages/GetLatestDeps.java
@@ -18,19 +18,22 @@
 
 package accord.messages;
 
+import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import accord.local.Command;
 import accord.local.DepsCalculator;
+import accord.local.DepsCalculator.AbstractDepsReply;
 import accord.local.LoadKeys;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
 import accord.local.Node.Id;
 import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
 import accord.local.StoreParticipants;
 import accord.primitives.Ballot;
 import accord.primitives.Deps;
+import accord.primitives.Known.KnownDeps;
 import accord.primitives.LatestDeps;
 import accord.primitives.PartialDeps;
 import accord.primitives.Route;
@@ -44,7 +47,7 @@ import accord.utils.async.Cancellable;
 import static accord.messages.MessageType.StandardMessage.GET_LATEST_DEPS_REQ;
 import static accord.messages.MessageType.StandardMessage.GET_LATEST_DEPS_RSP;
 
-public class GetLatestDeps extends RouteRequest.WithUnsynced<GetLatestDeps.GetLatestDepsReply>
+public class GetLatestDeps extends RouteRequest.WithUnsynced<ReplyList<GetLatestDeps.GetLatestDepsReply>>
 {
     public static final class SerializationSupport
     {
@@ -78,7 +81,14 @@ public class GetLatestDeps extends RouteRequest.WithUnsynced<GetLatestDeps.GetLa
     }
 
     @Override
-    public GetLatestDepsReply applyInternal(SafeCommandStore safeStore)
+    protected void acceptInternal(ReplyList<GetLatestDepsReply> replies, Throwable failure)
+    {
+        if (failure != null) acceptReply(null, failure);
+        else ReplyList.invoke(replies, GetLatestDepsReply::reduce, this::acceptReply);
+    }
+
+    @Override
+    public ReplyList<GetLatestDepsReply> applyInternal(SafeCommandStore safeStore)
     {
         StoreParticipants participants = StoreParticipants.read(safeStore, scope, txnId, minEpoch, executeAt.epoch());
         SafeCommand safeCommand = safeStore.get(txnId, participants);
@@ -86,26 +96,28 @@ public class GetLatestDeps extends RouteRequest.WithUnsynced<GetLatestDeps.GetLa
         if (ballot != null)
         {
             if (command.promised().compareTo(ballot) > 0)
-                return GetLatestDepsNack.INSTANCE;
+                return GetLatestDepsReply.NACK;
             command = safeCommand.updatePromised(ballot);
         }
+
         PartialDeps coordinatedDeps = command.partialDeps();
-        Deps localDeps = null;
-        if (!command.known().deps().hasCommittedOrDecidedDeps() && !command.hasBeen(Status.Truncated))
+        KnownDeps knownDeps = command.known().deps();
+        Ballot acceptedOrCommitted = command.acceptedOrCommitted();
+        if (knownDeps.hasCommittedOrDecidedDeps() || command.hasBeen(Status.Truncated))
         {
-            localDeps = DepsCalculator.calculateDeps(safeStore, txnId, participants, minEpoch, txnId, false);
+            LatestDeps deps = LatestDeps.create(participants.owns(), knownDeps, acceptedOrCommitted, coordinatedDeps, null);
+            return new GetLatestDepsReply(deps);
         }
 
-        LatestDeps deps = LatestDeps.create(participants.owns(), command.known().deps(), command.acceptedOrCommitted(), coordinatedDeps, localDeps);
-        return new GetLatestDepsOk(deps);
+        //noinspection resource
+        GetLatestDepsCalculator calculator = new GetLatestDepsCalculator(txnId, participants, knownDeps, acceptedOrCommitted, coordinatedDeps);
+        return calculator.calculate(safeStore, minEpoch, false);
     }
 
     @Override
-    public GetLatestDepsReply reduce(GetLatestDepsReply r1, GetLatestDepsReply r2)
+    public ReplyList<GetLatestDepsReply> reduce(ReplyList<GetLatestDepsReply> r1, ReplyList<GetLatestDepsReply> r2)
     {
-        if (!r1.isOk()) return r1;
-        if (!r2.isOk()) return r2;
-        return new GetLatestDepsOk(LatestDeps.merge(((GetLatestDepsOk)r1).deps, ((GetLatestDepsOk)r2).deps));
+        return ReplyList.merge(r1, r2);
     }
 
     @Override
@@ -131,47 +143,61 @@ public class GetLatestDeps extends RouteRequest.WithUnsynced<GetLatestDeps.GetLa
     }
 
     @Override
-    public LoadKeysFor loadKeysFor()
+    public FindKeys findKeys()
     {
-        return LoadKeysFor.READ_WRITE;
+        return FindKeys.CONFLICTS;
     }
 
-    public interface GetLatestDepsReply extends Reply
+    static class GetLatestDepsCalculator extends DepsCalculator.DepsReplyCalculator<GetLatestDepsReply> implements Function<Void, GetLatestDepsReply>
     {
-        boolean isOk();
-    }
+        final StoreParticipants participants;
+        final KnownDeps known;
+        final Ballot acceptedOrCommitted;
+        final Deps coordinatedDeps;
 
-    public static final class GetLatestDepsNack implements GetLatestDepsReply
-    {
-        public static final GetLatestDepsNack INSTANCE = new GetLatestDepsNack();
-        private GetLatestDepsNack(){}
-
-        @Override
-        public boolean isOk()
+        public GetLatestDepsCalculator(TxnId txnId, StoreParticipants participants, KnownDeps known, Ballot acceptedOrCommitted, Deps coordinatedDeps)
         {
-            return false;
+            super(txnId, txnId, participants);
+            this.participants = participants;
+            this.known = known;
+            this.acceptedOrCommitted = acceptedOrCommitted;
+            this.coordinatedDeps = coordinatedDeps;
         }
 
-        @Override
-        public MessageType type()
+        public GetLatestDepsReply apply(Void ignore)
         {
-            return GET_LATEST_DEPS_RSP;
+            try
+            {
+                Deps localDeps = deps();
+                LatestDeps deps = LatestDeps.create(participants.owns(), known, acceptedOrCommitted, coordinatedDeps, localDeps);
+                return new GetLatestDepsReply(deps);
+            }
+            finally
+            {
+                close();
+            }
         }
     }
 
-    public static class GetLatestDepsOk implements GetLatestDepsReply
+    public static class GetLatestDepsReply extends AbstractDepsReply<GetLatestDepsReply>
     {
+        public static final GetLatestDepsReply NACK = new GetLatestDepsReply();
         public final LatestDeps deps;
 
-        public GetLatestDepsOk(@Nonnull LatestDeps deps)
+        public GetLatestDepsReply(@Nonnull LatestDeps deps)
         {
             this.deps = Invariants.nonNull(deps);
         }
 
+        private GetLatestDepsReply()
+        {
+            this.deps = null;
+        }
+
         @Override
         public String toString()
         {
-            return "GetLatestDepsOk{" + deps + '}' ;
+            return "GetLatestDepsReply{" + deps + '}' ;
         }
 
         @Override
@@ -180,11 +206,16 @@ public class GetLatestDeps extends RouteRequest.WithUnsynced<GetLatestDeps.GetLa
             return GET_LATEST_DEPS_RSP;
         }
 
-        @Override
         public boolean isOk()
         {
-            return true;
+            return deps != null;
         }
-    }
 
+        private static GetLatestDepsReply reduce(GetLatestDepsReply r1, GetLatestDepsReply r2)
+        {
+            if (!r1.isOk()) return r1;
+            if (!r2.isOk()) return r2;
+            return new GetLatestDepsReply(LatestDeps.merge(r1.deps, r2.deps));
+        }
+    }
 }
diff --git a/accord-core/src/main/java/accord/messages/GetMaxConflict.java b/accord-core/src/main/java/accord/messages/GetMaxConflict.java
index c03a2e80..d566d7a0 100644
--- a/accord-core/src/main/java/accord/messages/GetMaxConflict.java
+++ b/accord-core/src/main/java/accord/messages/GetMaxConflict.java
@@ -63,6 +63,12 @@ public class GetMaxConflict extends RouteRequest.WithUnsynced<GetMaxConflict.Get
         return node.commandStores().mapReduceConsume(minEpoch, executionEpoch, this);
     }
 
+    @Override
+    protected void acceptInternal(GetMaxConflictOk reply, Throwable failure)
+    {
+        acceptReply(reply, failure);
+    }
+
     @Override
     public GetMaxConflictOk applyInternal(SafeCommandStore safeStore)
     {
diff --git a/accord-core/src/main/java/accord/messages/InformDurable.java b/accord-core/src/main/java/accord/messages/InformDurable.java
index e354a74b..c163150a 100644
--- a/accord-core/src/main/java/accord/messages/InformDurable.java
+++ b/accord-core/src/main/java/accord/messages/InformDurable.java
@@ -143,6 +143,12 @@ public class InformDurable extends RouteRequest<Reply> implements ExecutionConte
         return node.commandStores().mapReduceConsume(minEpoch, maxEpoch, this);
     }
 
+    @Override
+    protected void acceptInternal(Reply reply, Throwable failure)
+    {
+        acceptReply(reply, failure);
+    }
+
     @Override
     public Reply applyInternal(SafeCommandStore safeStore)
     {
diff --git a/accord-core/src/main/java/accord/messages/NoWaitRequest.java b/accord-core/src/main/java/accord/messages/NoWaitRequest.java
index 10de253c..a5b3b8f1 100644
--- a/accord-core/src/main/java/accord/messages/NoWaitRequest.java
+++ b/accord-core/src/main/java/accord/messages/NoWaitRequest.java
@@ -37,7 +37,7 @@ import accord.utils.async.Cancellable;
 import static accord.utils.Invariants.illegalState;
 import static java.util.concurrent.TimeUnit.MICROSECONDS;
 
-public abstract class NoWaitRequest<P extends Participants<?>, R extends Reply> extends AbstractRequest<P, R> implements Timeouts.Timeout
+public abstract class NoWaitRequest<P extends Participants<?>, R> extends AbstractRequest<P, R> implements Timeouts.Timeout
 {
     public static final CancellationException CANCELLATION_EXCEPTION = new CancellationException();
 
@@ -127,27 +127,31 @@ public abstract class NoWaitRequest<P extends Participants<?>, R extends Reply>
         acceptInternal(reply, failure);
     }
 
-    protected void acceptInternal(R reply, Throwable failure)
+    protected abstract void acceptInternal(R reply, Throwable failure);
+
+    protected void acceptReply(Reply reply, Throwable failure)
     {
-        if (reply == null && failure == null)
         {
-            Invariants.require(isCancelled());
-            if (!(replyContext instanceof LocalDelivery<?>))
+            if (reply == null && failure == null)
             {
-                if (tracing() != null)
-                   tracing().trace(null, "Completed with no reply");
-                return; // for now we don't report cancellation/timeout remotely, and rely on the coordinator's timeouts
+                Invariants.require(isCancelled());
+                if (!(replyContext instanceof LocalDelivery<?>))
+                {
+                    if (tracing() != null)
+                        tracing().trace(null, "Completed with no reply");
+                    return; // for now we don't report cancellation/timeout remotely, and rely on the coordinator's timeouts
+                }
+                // we must report something for local delivery, as we rely on this callback instead of registering a separate timeout
+                failure = CANCELLATION_EXCEPTION;
             }
-            // we must report something for local delivery, as we rely on this callback instead of registering a separate timeout
-            failure = CANCELLATION_EXCEPTION;
-        }
-        if (failure != null || reply.isFinal())
-        {
-            Invariants.require(!hasSentFinalReply);
-            hasSentFinalReply = true;
+            if (failure != null || reply.isFinal())
+            {
+                Invariants.require(!hasSentFinalReply);
+                hasSentFinalReply = true;
+            }
+            if (failure != null) cancel();
+            node.reply(replyTo, replyContext, reply, failure, tracing());
         }
-        if (failure != null) cancel();
-        node.reply(replyTo, replyContext, reply, failure, tracing());
     }
 
     @Override
diff --git a/accord-core/src/main/java/accord/messages/ParticipantsRequest.java b/accord-core/src/main/java/accord/messages/ParticipantsRequest.java
index 7d3f240e..1d308f5c 100644
--- a/accord-core/src/main/java/accord/messages/ParticipantsRequest.java
+++ b/accord-core/src/main/java/accord/messages/ParticipantsRequest.java
@@ -38,7 +38,7 @@ import accord.utils.async.Cancellable;
 import static accord.topology.Shard.Flag.MUST_WITNESS;
 import static accord.utils.Invariants.illegalArgument;
 
-public abstract class ParticipantsRequest<P extends Participants<?>, R extends Reply> extends NoWaitRequest<P, R>
+public abstract class ParticipantsRequest<P extends Participants<?>, R> extends NoWaitRequest<P, R>
 {
     public final long waitForEpoch;
 
diff --git a/accord-core/src/main/java/accord/messages/PreAccept.java b/accord-core/src/main/java/accord/messages/PreAccept.java
index 77f3e32d..8c8d3a97 100644
--- a/accord-core/src/main/java/accord/messages/PreAccept.java
+++ b/accord-core/src/main/java/accord/messages/PreAccept.java
@@ -19,6 +19,7 @@
 package accord.messages;
 
 import java.util.Objects;
+import java.util.function.Function;
 import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
@@ -27,17 +28,19 @@ import org.slf4j.LoggerFactory;
 import accord.coordinate.ExecuteFlag.ExecuteFlags;
 import accord.local.Command;
 import accord.local.Commands;
-import accord.local.DepsCalculator;
+import accord.local.DepsCalculator.AbstractDepsReply;
+import accord.local.DepsCalculator.DepsReplyCalculator;
 import accord.local.LoadKeys;
-import accord.local.LoadKeysFor;
+import accord.local.FindKeys;
+import accord.local.Node;
 import accord.local.Node.Id;
 import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
-import accord.primitives.PartialDeps;
 import accord.local.StoreParticipants;
 import accord.messages.RouteRequest.WithUnsynced;
 import accord.primitives.Deps;
 import accord.primitives.FullRoute;
+import accord.primitives.PartialDeps;
 import accord.primitives.PartialTxn;
 import accord.primitives.Route;
 import accord.primitives.Status;
@@ -49,11 +52,12 @@ import accord.utils.Invariants;
 import accord.utils.UnhandledEnum;
 import accord.utils.async.Cancellable;
 
+import static accord.api.ProtocolModifiers.loadKeysAsyncIfPermitted;
 import static accord.messages.MessageType.StandardMessage.PRE_ACCEPT_REQ;
 import static accord.messages.MessageType.StandardMessage.PRE_ACCEPT_RSP;
 import static accord.primitives.Timestamp.Flag.REJECTED;
 
-public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
+public class PreAccept extends WithUnsynced<ReplyList<PreAccept.PreAcceptReply>>
 {
     @SuppressWarnings("unused")
     private static final Logger logger = LoggerFactory.getLogger(PreAccept.class);
@@ -98,13 +102,13 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
     @Override
     public LoadKeys loadKeys()
     {
-        return LoadKeys.SYNC;
+        return loadKeysAsyncIfPermitted(txnId);
     }
 
     @Override
-    public LoadKeysFor loadKeysFor()
+    public FindKeys findKeys()
     {
-        return LoadKeysFor.READ_WRITE;
+        return FindKeys.CONFLICTS;
     }
 
     @Override
@@ -120,7 +124,14 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
     }
 
     @Override
-    public PreAcceptReply applyInternal(SafeCommandStore safeStore)
+    protected void acceptInternal(ReplyList<PreAcceptReply> replies, Throwable failure)
+    {
+        if (failure != null) acceptReply(null, failure);
+        else ReplyList.invoke(replies, PreAcceptReply::reduce, this::acceptReply);
+    }
+
+    @Override
+    public ReplyList<PreAcceptReply> applyInternal(SafeCommandStore safeStore)
     {
         StoreParticipants participants = StoreParticipants.update(safeStore, route, minEpoch, txnId, acceptEpoch);
         SafeCommand safeCommand = safeStore.get(txnId, participants);
@@ -142,25 +153,13 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
                     return new PreAcceptOk(txnId, command.executeAt(), Deps.NONE, ExecuteFlags.none());
 
             case Retired:
-                Timestamp executeAt;
-                ExecuteFlags flags;
-                Deps deps;
-                try (DepsCalculator calculator = new DepsCalculator(txnId))
-                {
-                    deps = calculator.calculate(safeStore, txnId, participants, minEpoch, txnId, true);
-                    if (deps == null)
-                        return PreAcceptNack.INSTANCE;
-                    flags = calculator.executeFlags(txnId);
-                    executeAt = calculator.executeAt(safeCommand, node);
-                }
-
-                // NOTE: we CANNOT test whether we adopt a future dependency here because it might be that this command
-                // is guaranteed to not reach agreement, but that this replica is unaware of that fact and has pruned
-                // all preceding transactions. In which case we may be able to adopt a future dependency but won't propose it.
-                // We do however prohibit later epochs as dependencies as we cannot handle those effectively
-                // when back-filling for execution of the transaction.
-                Invariants.require(deps.maxTxnId(txnId).epoch() <= txnId.epoch());
-                return new PreAcceptOk(txnId, executeAt, deps, flags);
+                Timestamp witnessedAt = command.executeAtOrTxnId(); // if retired, executeAt may be null
+                //noinspection resource
+                PreAcceptDepsCalculator calculator = new PreAcceptDepsCalculator(txnId, witnessedAt, participants, node);
+                ReplyList<PreAcceptReply> reply = calculator.calculate(safeStore, minEpoch, true);
+                if (reply == null)
+                    return PreAcceptNack.INSTANCE;
+                return reply;
 
             case Truncated:
             case RejectedBallot:
@@ -169,9 +168,9 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
     }
 
     @Override
-    public PreAcceptReply reduce(PreAcceptReply r1, PreAcceptReply r2)
+    public ReplyList<PreAcceptReply> reduce(ReplyList<PreAcceptReply> r1, ReplyList<PreAcceptReply> r2)
     {
-        return PreAcceptReply.reduce(r1, r2);
+        return ReplyList.merge(r1, r2);
     }
 
     @Override
@@ -180,7 +179,7 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
         return PRE_ACCEPT_REQ;
     }
 
-    public static abstract class PreAcceptReply implements Reply
+    public static abstract class PreAcceptReply extends AbstractDepsReply<PreAcceptReply>
     {
         @Override
         public MessageType type()
@@ -209,6 +208,41 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
         }
     }
 
+    public static class PreAcceptDepsCalculator extends DepsReplyCalculator<PreAcceptReply> implements Function<Void, PreAcceptReply>
+    {
+        final Node node;
+        final Timestamp witnessedAt;
+
+        public PreAcceptDepsCalculator(TxnId txnId, Timestamp witnessedAt, StoreParticipants participants, Node node)
+        {
+            super(txnId, txnId, participants);
+            this.node = node;
+            this.witnessedAt = Invariants.nonNull(witnessedAt);
+        }
+
+        public PreAcceptOk apply(Void ignore)
+        {
+            try
+            {
+                Deps deps = deps();
+                Timestamp executeAt = executeAt(witnessedAt, node);
+                ExecuteFlags flags = executeFlags();
+
+                // NOTE: we CANNOT test whether we adopt a future dependency here because it might be that this command
+                // is guaranteed to not reach agreement, but that this replica is unaware of that fact and has pruned
+                // all preceding transactions. In which case we may be able to adopt a future dependency but won't propose it.
+                // We do however prohibit later epochs as dependencies as we cannot handle those effectively
+                // when back-filling for execution of the transaction.
+                Invariants.require(deps.maxTxnId(txnId).epoch() <= txnId.epoch());
+                return new PreAcceptOk(txnId, executeAt, deps, flags);
+            }
+            finally
+            {
+                close();
+            }
+        }
+    }
+
     public static class PreAcceptOk extends PreAcceptReply
     {
         public final TxnId txnId;
@@ -219,7 +253,7 @@ public class PreAccept extends WithUnsynced<PreAccept.PreAcceptReply>
         public PreAcceptOk(TxnId txnId, Timestamp witnessedAt, Deps deps, ExecuteFlags flags)
         {
             this.txnId = txnId;
-            this.witnessedAt = witnessedAt;
+            this.witnessedAt = Invariants.nonNull(witnessedAt);
             this.deps = deps;
             this.flags = flags;
         }
diff --git a/accord-core/src/main/java/accord/messages/ReadData.java b/accord-core/src/main/java/accord/messages/ReadData.java
index fd13a668..b0258ed2 100644
--- a/accord-core/src/main/java/accord/messages/ReadData.java
+++ b/accord-core/src/main/java/accord/messages/ReadData.java
@@ -35,6 +35,7 @@ import accord.local.Command;
 import accord.local.Command.Committed;
 import accord.local.CommandStore;
 import accord.local.CommandStores;
+import accord.local.LoadKeys;
 import accord.local.Node;
 import accord.local.SafeCommand;
 import accord.local.SafeCommandStore;
@@ -737,14 +738,6 @@ public abstract class ReadData extends AbstractRequest<Participants<?>, ReadData
         }
     }
 
-    @Override
-    public Unseekables<?> keys()
-    {
-        if (flags.contains(READY_TO_EXECUTE) && fastReadsMayBypassCommandsForKey(txnId))
-            return RoutingKeys.EMPTY;
-        return scope;
-    }
-
     protected void reply(Ranges unavailable, Data data, long uniqueHlc)
     {
         if (data != null && !txnId.awaitsOnlyDeps() && !data.validateReply(txnId, executeAt, validateHlc()))
diff --git a/accord-core/src/main/java/accord/messages/ReplyList.java b/accord-core/src/main/java/accord/messages/ReplyList.java
new file mode 100644
index 00000000..452c6c13
--- /dev/null
+++ b/accord-core/src/main/java/accord/messages/ReplyList.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.messages;
+
+import java.util.ArrayList;
+import java.util.function.BiConsumer;
+
+import accord.utils.Reduce;
+import accord.utils.async.AsyncResults;
+import accord.utils.async.CancellableAsyncResult;
+
+public interface ReplyList<R extends CancellableAsyncResult<R> & ReplyList<R>>
+{
+    int size();
+    CancellableAsyncResult<R> get(int i);
+    void cancelReplies();
+
+    final class ReplyMultiList<R extends CancellableAsyncResult<R> & ReplyList<R>> extends ArrayList<CancellableAsyncResult<R>> implements ReplyList<R>
+    {
+        public ReplyMultiList() {}
+        public ReplyMultiList(int initialCapacity) { super(initialCapacity); }
+
+        @Override
+        public void cancelReplies()
+        {
+            forEach(CancellableAsyncResult::cancel);
+        }
+    }
+
+    // parameters must be EITHER a ReplyMultiList<R> OR a single R (that itself implements both ReplyList<R> and AsyncResult<R>)
+    static <R extends CancellableAsyncResult<R> & ReplyList<R>> ReplyList<R> merge(ReplyList<R> r1, ReplyList<R> r2)
+    {
+        if (r1 == null || r2 == null)
+        {
+            if (r1 != null) r1.cancelReplies();
+            if (r2 != null) r2.cancelReplies();
+            return null;
+        }
+
+        ReplyMultiList<R> multi;
+        if (r1 instanceof ReplyMultiList)
+        {
+            multi = (ReplyMultiList<R>) r1;
+            if (r2 instanceof ReplyMultiList)
+            {
+                multi.addAll((ReplyMultiList<R>)r2);
+                return multi;
+            }
+            multi.add((CancellableAsyncResult<R>) r2);
+        }
+        else if (r2 instanceof ReplyMultiList)
+        {
+            multi = (ReplyMultiList<R>) r2;
+            multi.add((CancellableAsyncResult<R>) r1);
+        }
+        else
+        {
+            multi = new ReplyMultiList<>();
+            multi.add((CancellableAsyncResult<R>) r1);
+            multi.add((CancellableAsyncResult<R>) r2);
+        }
+        return multi;
+    }
+
+    static <R extends CancellableAsyncResult<R> & ReplyList<R>> void invoke(ReplyList<R> replies, Reduce<R, R> reduce, BiConsumer<? super R, Throwable> callback)
+    {
+        if (replies instanceof ReplyMultiList<?>)
+        {
+            AsyncResults.reduce((ReplyMultiList<R>)replies, reduce)
+                        .invoke(callback);
+        }
+        else
+        {
+            ((R)replies).invoke(callback);
+        }
+    }
+}
diff --git a/accord-core/src/main/java/accord/messages/RouteRequest.java b/accord-core/src/main/java/accord/messages/RouteRequest.java
index 3a070444..21891165 100644
--- a/accord-core/src/main/java/accord/messages/RouteRequest.java
+++ b/accord-core/src/main/java/accord/messages/RouteRequest.java
@@ -27,9 +27,9 @@ import accord.primitives.TxnId;
 import accord.topology.Topologies;
 import accord.utils.async.Cancellable;
 
-public abstract class RouteRequest<R extends Reply> extends ParticipantsRequest<Route<?>, R>
+public abstract class RouteRequest<R> extends ParticipantsRequest<Route<?>, R>
 {
-    public static abstract class WithUnsynced<R extends Reply> extends RouteRequest<R>
+    public static abstract class WithUnsynced<R> extends RouteRequest<R>
     {
         public final long minEpoch; // TODO (low priority, clarity): can this just always be TxnId.epoch?
 
diff --git a/accord-core/src/main/java/accord/primitives/Deps.java b/accord-core/src/main/java/accord/primitives/Deps.java
index 862a2641..27a7b525 100644
--- a/accord-core/src/main/java/accord/primitives/Deps.java
+++ b/accord-core/src/main/java/accord/primitives/Deps.java
@@ -18,9 +18,17 @@
 
 package accord.primitives;
 
+import java.util.AbstractList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+
 import accord.api.RoutingKey;
 import accord.local.cfk.CommandsForKey;
-import accord.primitives.Routable.Domain;
 import accord.utils.IndexedFunction;
 import accord.utils.Invariants;
 import accord.utils.MergeFewDisjointSortedListsCursor;
@@ -32,19 +40,9 @@ import accord.utils.SortedList.MergeCursor;
 import accord.utils.TriFunction;
 import accord.utils.UnhandledEnum;
 
-import java.util.AbstractList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.BiFunction;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import javax.annotation.Nullable;
-
 import static accord.local.cfk.CommandsForKey.managesExecution;
 import static accord.primitives.Routables.Slice.Minimal;
 import static accord.primitives.Timestamp.Flag.UNSTABLE;
-import static accord.utils.Invariants.illegalState;
 
 /**
  * A collection of transaction dependencies, keyed by the key or range on which they were adopted.
@@ -105,14 +103,6 @@ public class Deps
             this.rangeBuilder = buildRangesByTxnId ? RangeDeps.byTxnIdBuilder() : RangeDeps.builderByRange();
         }
 
-        public AbstractBuilder<T> addNormalise(Unseekable keyOrRange, TxnId txnId)
-        {
-            if (keyOrRange.domain() == txnId.domain()) add(keyOrRange, txnId);
-            else if (keyOrRange.domain() == Domain.Key) add(keyOrRange.asRange(), txnId);
-            else throw illegalState();
-            return this;
-        }
-
         public AbstractBuilder<T> add(Unseekable keyOrRange, TxnId txnId)
         {
             Invariants.requireArgument(keyOrRange.domain() == txnId.domain(), "%s is not same domain as %s", keyOrRange,  txnId);
diff --git a/accord-core/src/main/java/accord/utils/async/AsyncChain.java b/accord-core/src/main/java/accord/utils/async/AsyncChain.java
index cda831db..8a22482a 100644
--- a/accord-core/src/main/java/accord/utils/async/AsyncChain.java
+++ b/accord-core/src/main/java/accord/utils/async/AsyncChain.java
@@ -165,6 +165,11 @@ public interface AsyncChain<V>
 
     default AsyncResult<V> beginAsResult()
     {
-        return AsyncResults.forChain(this);
+        return AsyncResults.begin(this);
+    }
+
+    default CancellableAsyncResult<V> beginAsCancellableResult()
+    {
+        return AsyncResults.beginCancellable(this);
     }
 }
\ No newline at end of file
diff --git a/accord-core/src/main/java/accord/utils/async/AsyncChains.java b/accord-core/src/main/java/accord/utils/async/AsyncChains.java
index 5d334433..bca728d9 100644
--- a/accord-core/src/main/java/accord/utils/async/AsyncChains.java
+++ b/accord-core/src/main/java/accord/utils/async/AsyncChains.java
@@ -440,7 +440,6 @@ public class AsyncChains
         }
     }
 
-
     private static class DetectLeak extends AsyncChains.Head<Void>
     {
         private final AtomicBoolean called = new AtomicBoolean(false);
diff --git a/accord-core/src/main/java/accord/utils/async/AsyncResults.java b/accord-core/src/main/java/accord/utils/async/AsyncResults.java
index cc794831..e108b504 100644
--- a/accord-core/src/main/java/accord/utils/async/AsyncResults.java
+++ b/accord-core/src/main/java/accord/utils/async/AsyncResults.java
@@ -209,7 +209,7 @@ public class AsyncResults
         }
     }
 
-    static class Chain<V> extends AbstractResult<V>
+    public static class Chain<V> extends AbstractResult<V> implements BiConsumer<V, Throwable>
     {
         private AsyncChain<V> chain;
         public Chain(AsyncChain<V> chain)
@@ -221,8 +221,60 @@ public class AsyncResults
         @Override
         void setResult(V result, Throwable failure)
         {
+            chain = null;
             super.setResult(result, failure);
+        }
+
+        @Override
+        public void accept(V success, Throwable fail)
+        {
+            setResult(success, fail);
+        }
+
+        @Override
+        public String toString()
+        {
+            AsyncChain<V> chain = this.chain;
+            if (chain != null)
+                return "Waiting On: " + chain;
+            return super.toString();
+        }
+    }
+
+    public static class CancellableChain<V> extends AbstractResult<V> implements Cancellable, BiConsumer<V, Throwable>, CancellableAsyncResult<V>
+    {
+        private AsyncChain<V> chain;
+        private Cancellable cancel;
+
+        public CancellableChain(AsyncChain<V> chain)
+        {
+            this.chain = chain;
+            this.cancel = chain.begin(this);
+        }
+
+        @Override
+        void setResult(V result, Throwable failure)
+        {
             chain = null;
+            cancel = null;
+            super.setResult(result, failure);
+        }
+
+        @Override
+        public void cancel()
+        {
+            Cancellable cancel = this.cancel;
+            if (cancel == null)
+                return;
+            this.chain = null;
+            this.cancel = null;
+            cancel.cancel();
+        }
+
+        @Override
+        public void accept(V success, Throwable fail)
+        {
+            setResult(success, fail);
         }
 
         @Override
@@ -307,7 +359,7 @@ public class AsyncResults
         }
     }
 
-    static abstract class AbstractImmediate<V> implements AsyncResult<V>
+    public static abstract class AbstractImmediate<V> implements AsyncResult<V>
     {
         @Override
         public AsyncChain<V> chain()
@@ -391,11 +443,19 @@ public class AsyncResults
     /**
      * Creates an AsyncResult for the given chain. This calls begin on the supplied chain
      */
-    public static <V> AsyncResult<V> forChain(AsyncChain<V> chain)
+    public static <V> AsyncResult<V> begin(AsyncChain<V> chain)
     {
         return new Chain<>(chain);
     }
 
+    /**
+     * Creates an AsyncResult for the given chain. This calls begin on the supplied chain
+     */
+    public static <V> CancellableAsyncResult<V> beginCancellable(AsyncChain<V> chain)
+    {
+        return new CancellableChain<>(chain);
+    }
+
     public static <V> AsyncResult<V> success(V value)
     {
         if (value == null)
diff --git a/accord-core/src/main/java/accord/utils/async/CancellableAsyncResult.java b/accord-core/src/main/java/accord/utils/async/CancellableAsyncResult.java
new file mode 100644
index 00000000..1085a10f
--- /dev/null
+++ b/accord-core/src/main/java/accord/utils/async/CancellableAsyncResult.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils.async;
+
+public interface CancellableAsyncResult<V> extends AsyncResult<V>, Cancellable
+{
+}


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