Subject: Re: [DNSOP] I-D Action: draft-ietf-dnsop-rfc5011-security-considerations-08.txt

From: Michael StJohns <[email-masked]>
Date: Sun, 3 Dec 2017 18:18:18 -0500

On 11/29/2017 5:29 PM, Wes Hardaker wrote:
> [email-masked] writes:
>
>> A New Internet-Draft is available from the on-line Internet-Drafts
>> directories.  This draft is a work item of the Domain Name System
>> Operations WG of the IETF.
> FYI, this contains the restructuring talked about during IETF100/dnsop
> as well the new safetyMargin concepts proposed by MSJ.  I haven't done a
> complete double check on all my restructuring yet, so for the chairs
> especially: there will likely be a -09 very soon ready for second WGLC,
> but not this one.

Hi -

Much improved - but still some disconnects (all review is de novo):

In Abstract - insert "by the publisher" after "must be followed" - this 
is clear later, but should be clear in the abstract.

in Section 2 - first para - "from the DNSKEY publication and 
revocation's point of view" is unusual phrasing.  I'm not sure how a 
publication or revocation has a point of view.  I think you meant from 
the trust anchor publisher or SEP DNSKEY publisher's point of view?

in 3 - lastSigExpirationTime - replace the first sentence  with "The 
latest value (i.e. the future most date and time) of any RRSig Signature 
Expiration field  covering any DNSKEY RRSet containing only the old 
trust anchor(s) that are being superseded." - This may still need 
wordsmithing or expansion.

in 3 - sigExpirationTimeRemaining - two items: "latestSigExpirationTime" 
-> lastSigExpirationTime.  and measured from when?   I think its "when 
the addWaitTime calculation is run" or "lastSigExpirationTime - now"

in 5.1.1 T+10 - replace "they have now expired" with "the signatures 
have now expired" -  clarify context.

Delete 6.1.4 - activeRefreshOffset -  its a nonsensical value that is 
only valid from the resolver's point of view.   For a given 
publisher/authoritative server - there will be as many 
activeRefreshOffsets as there are resolvers so the publisher must assume 
the worst case of activeRefresh.

In 6.2.1 - replace activeRefreshOffset with activeRefresh - worst case 
value.

fix 6.2.1.1 delete the term for addHoldDownTime % activeRefresh - the "2 
* activeRefresh" in the previous term covers both the activeRefresh 
interval at the beginning of the acceptance period and the activeRefresh 
interval at the end.

In 6.2.2 - same changes as for 6.2.1 and 6.2.1.1 (e.g. get rid of 
activeRefreshOffset throughout).

                         v          activeRefresh v     
addHoldDownTime                 v    activeRefresh v  safetyMargin  v

|-----------------------|-------------------------------------|-------------------------------------|--------------------------|----------------|

  lastSigExpirationTime^^^                 acceptanceStarts ^^^       
acceptance begins to complete^^            earliestSafe^^^

After the second activeRefresh interval all of the well behaved and well 
connected resolvers should have the new trust anchor. The safetyMargin 
adds some space for poorly behaving or intermittently connected 
resolvers or those with some drops in queries.

Section 6.3 has one too many activeRefresh terms in both formulas - here 
are the corrected ones:

remWaitTime = sigExpirationTimeRemaining + activeRefresh + safetyFactor

remWallClockTime = lastSigExpirationTime + activeRefresh + safetyFactor

Basically, assuming no attacker, and no drops, all well-behaved 
resolvers will see the revocation after one activeRefresh interval from 
the time of publication.  Add the safety factor to take care of the 
slackers.  This is a fine value for normal revocations where you're 
pretty sure that the key hasn't been compromised.

There is no hold-time timer for revocation - they take effect 
immediately upon receipt and validation.

In the case of a key compromise, I would suggest that the revoked key be 
published for the same interval as you would use for adding a new trust 
anchor.  (But of course, this won't actually matter all that much if you 
only have a single trust anchor....)

Appendix A - fix the calculations to match up with the section 6 formulas.

Later, Mike

---

From: Wes Hardaker <[email-masked]>
Date: Thu, 07 Dec 2017 16:53:25 -0800

> Much improved - but still some disconnects (all review is de novo):

That's Mike.  All good comments.  I've attached responses and actions
(or inactions) below and will push a new version shortly as well.

Wes Hardaker

Table of Contents
_________________

1 DONE In Abstract - insert "by the publisher" after "must be followed" -
2 DONE Section 2 - first para - "from the DNSKEY publication and
3 DONE in 3 - lastSigExpirationTime - replace the first sentence  with "The
4 TODO in 3 - sigExpirationTimeRemaining - two items:
5 DONE in 5.1.1 T+10 - replace "they have now expired" with "the signatures
6 WONTDO Delete 6.1.4 - activeRefreshOffset -  its a nonsensical value that is
7 WONTDO 6.2.1 - replace activeRefreshOffset with activeRefresh - worst case
8 WONTDO fix 6.2.1.1 delete the term for addHoldDownTime % activeRefresh - the
9 WONTDO In 6.2.2 - same changes as for 6.2.1 and 6.2.1.1 (e.g. get rid of
10 DONE Section 6.3 has one too many activeRefresh terms in both formulas -
11 WONTDO Guidance about key compromise
12 DONE Appendix A - fix the calculations to match up with the section 6 formulas.

1 DONE In Abstract - insert "by the publisher" after "must be followed" -
=========================================================================

  this is clear later, but should be clear in the abstract.

2 DONE Section 2 - first para - "from the DNSKEY publication and
================================================================

  + Response: I did indeed mean SEP publisher; fixed

3 DONE in 3 - lastSigExpirationTime - replace the first sentence  with "The
===========================================================================

  latest value (i.e. the future most date and time) of any RRSig
  Signature Expiration field  covering any DNSKEY RRSet containing only
  the old trust anchor(s) that are being superseded." - This may still
  need wordsmithing or expansion.

4 TODO in 3 - sigExpirationTimeRemaining - two items:
=====================================================

  "latestSigExpirationTime" -> lastSigExpirationTime.  and measured from
  when?   I think its "when the addWaitTime calculation is run" or
  "lastSigExpirationTime - now"

  + Response: I think it's fairly self evident from the text that it's
    "when used", which is indeed at least during addWaitTime
    calculation.  But it's more conceptual and 'the amount of time
    remaining' I think is pretty clear to mean "from now".  I thought
    about adding something like "from now" into the sentence, but that
    didn't seem better to me and added unneeded or complexity to the
    sentence.  Suggestions?

5 DONE in 5.1.1 T+10 - replace "they have now expired" with "the signatures
===========================================================================

6 WONTDO Delete 6.1.4 - activeRefreshOffset -  its a nonsensical value that is
==============================================================================

  only valid from the resolver's point of view.   For a given
  publisher/authoritative server - there will be as many
  activeRefreshOffsets as there are resolvers so the publisher must
  assume the worst case of activeRefresh.

  + Response: I disagree here.  This was put in after a discussion with
    Matthijs Mekking to specifically address the case where the polling
    period may not end up right in time-step with the addHoldDownTime.
    The end result is that when a RFC5011 resolver is polling at a
    different frequency based on the activeRefresh time.  We are
    defining a minimum mathematically defined safety value and this
    value is calculatable, so it should remain.  There is a hint of
    guidance text that says you can set it to activeRefresh for
    simplicity if you want.  But the math-line is before a full second
    activeRefresh.  But the value itself needs to be included to account
    for the odd frequency slippage.

    TL;DR: this isn't a guidance document, it's a mathematical line
    document

7 WONTDO 6.2.1 - replace activeRefreshOffset with activeRefresh - worst case
============================================================================

  value. (Wait Timer Based Calculation)

8 WONTDO fix 6.2.1.1 delete the term for addHoldDownTime % activeRefresh - the
==============================================================================

  "2 * activeRefresh" in the previous term covers both the activeRefresh
  interval at the beginning of the acceptance period and the
  activeRefresh interval at the end.

9 WONTDO In 6.2.2 - same changes as for 6.2.1 and 6.2.1.1 (e.g. get rid of
==========================================================================

  ,----
  |                         v          activeRefresh v    
  | addHoldDownTime                 v    activeRefresh v  safetyMargin  v
  | 
  | |-----------------------|-------------------------------------|-------------------------------------|--------------------------|----------------|
  | 
  |  lastSigExpirationTime^^^                 acceptanceStarts ^^^      
  | acceptance begins to complete^^            earliestSafe^^^
  `----

  After the second activeRefresh interval all of the well behaved and
  well connected resolvers should have the new trust anchor. The
  safetyMargin adds some space for poorly behaving or intermittently
  connected resolvers or those with some drops in queries.

10 DONE Section 6.3 has one too many activeRefresh terms in both formulas -
===========================================================================

  here are the corrected ones:

  remWaitTime = sigExpirationTimeRemaining + activeRefresh +
  safetyFactor

  remWallClockTime = lastSigExpirationTime + activeRefresh +
  safetyFactor

  Basically, assuming no attacker, and no drops, all well-behaved
  resolvers will see the revocation after one activeRefresh interval
  from the time of publication.  Add the safety factor to take care of
  the slackers.  This is a fine value for normal revocations where
  you're pretty sure that the key hasn't been compromised.

11 WONTDO Guidance about key compromise
=======================================

  In the case of a key compromise, I would suggest that the revoked key
  be published for the same interval as you would use for adding a new
  trust anchor.  (But of course, this won't actually matter all that
  much if you only have a single trust anchor....)

  + Good advice to put in an general advice document in the future

12 DONE Appendix A - fix the calculations to match up with the section 6 formulas.
==================================================================================

-- 
Wes Hardaker
USC/ISI

---

From: Michael StJohns <[email-masked]>
Date: Fri, 8 Dec 2017 00:43:36 -0500

> (or inactions) below and will push a new version shortly as well.

   The problem is that the polling period is a resolver concept, not a 
publisher concept.   And there are as many different polling intervals 
(all consisting of different start times but with lengths approximately 
equal to the activeRefresh interval) as there are resolvers.

Look.   Publisher publishes at time A -.000000001 (or any time before 
this) where A is the last expiration time of the old records.  Resolver 
First does an active query at A+.00000001, Resolver Last does a query at 
A + activeRefresh interval - .000000001.  All other well behaved 
resolvers will have made a query at or after the first resolver and at 
or before the last resolver (with a safety slop added in for lost 
packets and retransmits done under the fast query rate).    Resolver 
Last will then (along with all the other resolvers) wait at least 
addHoldDown time and then make one last query.  The relationship to when 
that last query is made to the resolver's perception of when the 
addHoldDownTime expired  can be different for each resolver, with the 
sole requirement that the resolver waited for the next query after the 
addHoldDownTime expired.

A perfectly operating resolver with perfect clock and perfect 
connectivity and no outages MIGHT possibly keep a perfect interval 
between each query it makes (making your activeRefreshOffset 
meaningful), but 10000 resolvers ALL keeping perfect intervals? NWIH.   
So for the purposes of this we assume that ALL resolvers addHoldDownTime 
expired just after their last query meaning that we assume that ALL 
resolvers waited at least another queryInterval after the addHoldTime 
before making the query that triggers trust anchor installation.

the calculations represent is the worst case for any give resolver, but 
actually the overall best case numbers for the publisher.   The 
safetyFactor deals with the clock and query drifts from the large 
population of resolvers and moves the publisher slightly off their best 
case estimate.

So the total interval from after the last RRSig expiration of the old 
stuff is  activeRefresh (the latest query from all well behaved 
resolvers after the last expiration) + addHoldDownTime (.0000001 after 
this is when the first resolver installs the trust anchor) + 
activeRefresh (when the well behaved and connected resolvers have 
installed the new trust anchors) + safetyFactor (when the guys who 
dropped a few packets installed during the process the trust anchors).

You got it wrong.  It's not a matter of disagreement about whether this 
is guidance  vs math, its about choosing the parameters in a way that's 
meaningful from the publisher's point of view.  You picked data that is 
only meaningful in the context of a single resolver, but which reduces 
to activeRefresh for a large population of resolvers.

Don't make it more complex than it needs to be.

> 7 WONTDO 6.2.1 - replace activeRefreshOffset with activeRefresh - worst case

Yes and no.  How you do revocation depends on why you are doing 
revocation and what the downside of the revocation is if you don't get 
it right.  In the one in/one out scheme that the root uses you can use 
the shorter publication period. If you're dealing with a compromised key 
you need different guidance.  Both of these are guidance for the 
publisher based on different input conditions. Ignoring one over the 
other makes little sense. The math i gave you is correct for the input 
conditions you specified.  But the aren't the only possible input 
conditions.  This document started because you believed that the 
publishers weren't making the right assumptions about publication 
intervals and needed some guidance to explain what they needed to think 
about.  This seems to be the same class of misguidance.  If a publisher 
uses this guidance for a compromised key they might (probably will) be 
making a mistake.

Later, Mike

---

From: Michael StJohns <[email-masked]>
Date: Fri, 08 Dec 2017 06:27:07 +0000

To try this out, let’s use a ttl of 28 hours and an expiration of 7 days to
get an active refresh as below.

Take an activeRefresh of 14 hours (giving a fast retry of 2.8 hours and an
addHoldDown time of 30 days (720 hour). That gives you an
activeRefreshOffset of 6 hours.   A perfect resolver will make 51 queries
in 714 hours and the last triggering query at 728 hours.

Assume a 4% failure rate (to make the math easy) in queries and assume the
first retry always works. Which adds approx two fast retry intervals to the
time making the total time to do 51 queries 719.8 hour for an actual offset
of .2 hours.  The next and last query needed to take place to trigger the
trust anchor installation will take place at 733.8 hours.

The point is that retransmission drift makes the whole concept of
activeRefreshOffset nonsensical in any population of resolvers with any
losses at all.

Mike

---

From: Paul Vixie <[email-masked]>
Date: Sun, 10 Dec 2017 22:57:52 -0800

> get an active refresh as below.

Mike, I very much appreciate the analysis of the text and the proposed method that 
you've done. I am picking this message as my entry point to this thread even though 
my comments are also applicable to Message-ID: <6d239b9a-fd1e-46a3-
[email-masked]>.

> activeRefreshOffset nonsensical in any population of resolvers with any

Authors, I suggest and request that you both model and simulate the proposed 
method, along the lines Mike has done in this thread but with more than one set of 
parameters and conditions, in order that the second-set-of-eyes can be readily 
provided by more than one reviewer.

This timing based approach to online DNSSEC signing key changes is subtle beyond 
anybody's expectations, and because it will be used by the root zone, it is vital that we 
do more than simply whiteboard our proposed methods.

-- 
P. Vixie
-- 
P. Vixie

---

From: Wes Hardaker <[email-masked]>
Date: Mon, 11 Dec 2017 17:03:09 -0800

Hi Mike,

Thanks for explaining your thinking because I think, after reading it:
we're actually in agreement but using different terms for where to put
in the slop you're worried about.

Specifically:

> meaningful), but 10000 resolvers ALL keeping perfect intervals?

Yes, I agree.  But, this is why I want the majority of the equation to
be defining the mathematical perfect certainty.  And then *after* that,
add the operational slop factor (safetyMargin) to account for both
problems and reality (you forgot to add "speed of light issues" in your
text above, for example).

Thus, I break the equation into two critical parts:

addWallClockTime = lastSigExpirationTime
                   + addHoldDownTime
                   + activeRefresh                   ^
                   + activeRefreshOffset             |
                                                     |
Precise Math                                         |
-----------------------------------------------------|
Needed Fuzz                                          |
                                                     |
                   + safetyMargin                    |
                                                     v

IE, if a perfect resolver hitting a RFC5011 zone with an activeRefresh
that evenly divides into 30 days:

  1) queries at T--- = lastSigExpirationTime - .000001
  2) queries at T+1--- = lastSigExpirationTime - .000001 + activeRefresh
  3) Notes that it just saw a new key (assuming worst case #1 is replayed)
  4) starts timer
  5) will query again at lastSigExpirationTime + 30 days - .000001
  6) notes this is still in waiting period
  7) will query again at lastSigExpirationTime + 30 days - .000001 + activeRefresh
  8) now notes that it's been 30 days and accepts key

There is only 1 activeRefresh in that sequence.  And that's what's in
the equation.  Because the timing distance between #7 and #2 is still 30
days when queried to the perfect sub-nano second.

Then there should be a bunch of delays inserted, network timeouts, etc.
That's where the safetyMargin should come in and catch all the issues
with the impreciseness of the real world.  Now, if you want to add an
activeRefresh to the already defined safetyMargin suggested term, I'm
willing to consider that.  But it shouldn't be listed as part of
anything but the slop term for security analysis clarity.

Would you like to add more time to the safetyMargin to deal with the
non-perfect world, including clock drift because of time delays in a
bunch of queries back to back or any other reason?

Ending note about the precise timeline: when 30 days isn't divisible by
the activeRefresh, then you need to add the other term we haven't talked
about much which is the activeRefreshOffset which accounts for this
case.

Cheers,
Wes

---

From: Michael StJohns <[email-masked]>
Date: Mon, 11 Dec 2017 21:14:35 -0500

On 12/11/2017 8:03 PM, Wes Hardaker wrote:
> text above, for example ).
(sigh - safety factor deals with speed of light issues....DUH)

No, no, no, no, no.

> addWallClockTime = lastSigExpirationTime

If you'd reorder this properly, you can probably get the right answer -  
For the first part of the discussion assume no drifts or retransmits 
between (1) and (2) and between (3) and (4).

1) T == lastSigExpirationTime  and microscopically after this the time 
that the server sees the first query from any resolver starting their 
trust anchor installation.
2) T + activeRefresh  is the time at which the server sees the last 
query from the last resolver just starting their trust anchor installation.
3) T + activeRefresh + addHoldDownTime is the time at which the server 
sees the first query from any resolver finalizing its trust anchor 
installation.
4) T + activeRefresh + addHoldDownTime + activeRefresh is the time at 
which the server sees the last query from the last resolver finalizing 
its trust anchor installation.

(1) is the earliest time any resolver can start its installation 
(assuming an attack) because its also the time when all of the old 
signatures expire.
(2) is the time at which a resolver who had its last activeRefresh just 
before T (and because of that wasn't able to start its installation) 
will send its first installation query.
Between (2) and (3) any given resolver may drift/retransmit with the 
result that any given resolver may end up making a query just before (3) 
placing its next and final query at (3) plus activeRefresh.

Finally, to deal with drift and retransmits between (1) and (2), and 
between (3) and (4) we add a safetyFactor.    That deals with about 
99.9999% of drift and retransmits but will never deal with the servers 
that have been offline or otherwise unable to get their queries 
completed.  The retransmits in a given clients addHoldDown period only 
really move the end point for a given resolver and don't affect the 
overall safetyFactor of the set of resolvers.

>    2) queries at T+1--- = lastSigExpirationTime - .000001 + activeRefresh
Yes.
>    5) will query again at lastSigExpirationTime + 30 days - .000001
No - from the servers point of view, the worst client (which is the only 
one the server cares about) will make its last query before trust anchor 
installation at lastSigExpirationTime + activeRefresh (when the last 
CLIENT saw its first valid update)  + 30 days -.0000001.
>    7) will query again at lastSigExpirationTime + 30 days - .000001 + activeRefresh
Nope.   The worst client will query again at (from the servers point of 
view) lastSigExpiration + activeRefresh + addHoldDown (30) + activeRefresh

 From a given client's point of view the last query can happen anywhere 
from (lastSigExpiration + addHoldDown + .00000001) to 
(lastSigExpriration + activeRefresh + addHoldDown + activeRefresh).   
The server only cares about the worst (latest) case.

> days when queried to the perfect sub-nano second.

Nope.  Not from the servers point of view.

> about much which is the activeRefreshOffset which accounts for this

And again. NO.  The retransmits over a given set of clients in the 
addHoldDown period will result in at least one client (the "worst" 
client) ending up making a query just before the expiration of ITS 
addHoldDown timer.   Assuming the worst case of at least one client 
making a query just before the lastSigExpirationTime and that same 
client drifting/retransmitting enough to make a query just before its 
addHoldDown time the activeRefreshOffset is a useless value to calculate.

Later, Mike

---

From: Wes Hardaker <[email-masked]>
Date: Tue, 12 Dec 2017 09:24:37 -0800

> sees the first query from any resolver finalizing its trust anchor

There is where we disagree.  Given 2, where you state "last query from
the last resolver is just starting", I argue that exactly an
addHoldDownTime beyond that is when that *exact same resolver* will
finish because it will have sampled the first at (2) and again at
exactly T + activeRefresh + addHoldDownTime and accept it, per 5011:

   Once the timer expires, the new key will be added as a trust anchor
   the next time the validated RRSet with the new key is seen at the
   resolver.

And the last query from the last resolver will be at T + activeRefresh +
addHoldDownTime.

> result that any given resolver may end up making a query just before
> (3) placing its next and final query at (3) plus activeRefresh.

Please forget drift in the top half of the equation.  There is zero
drift in the mathematically precise section.  We will deal with drift,
delays, and everything else in the safetyFactor alone, with many terms
or concepts within it to get it right.

> No - from the servers point of view, the worst client (which is the
> only one the server cares about) will make its last query before trust
> anchor installation at lastSigExpirationTime + activeRefresh (when the
> last CLIENT saw its first valid update)  + 30 days -.0000001.

Yes, I said that in 6 stating that it was *still waiting*.  IE, #5 was
supposed to describe the second to last query.

>>    6) notes this is still in waiting period

Let me put together something, per Paul's request, to work at this from
another angle where one of us can be shown right or wrong. 

[... retry, delay text by me deleted ...]  

> addHoldDown time the activeRefreshOffset is a useless value to

If you want to put an extra activeRefresh into the safetyMargin to
account for drift, I'm willing to do so.  Or we can insert a new term
labeled "driftSafetyMargin" and define it as activeRefresh if you want.
But that goes below my math line, not above it (and we can relabel
safetyMargin as retryFailureSafetyMargin).

>From a purely security analysis point of view, the first thing we have
to agree upon is the precise moment at which all clients in a perfect
world, with *no errors at all* (no drift, no retries, no transmission
line delays, no CPU processing delays, no clock failures, etc).  Once we
have this line in the sand in place, then we can introduce real-world
correctional elements to account for reality sneaking into our perfect
world.  I'm trying to talk only about the perfectionists world line in
the sand first, and then introduce needed operational components *after
that line*.  You keep inserting "drift" (eg) everywhere in the process
of this argument, which I absolutely agree needs to be dealt with.  But
below my perfect-world line only.  The way I keep reading everything
you've written is that your perfect line includes two activeRefreshes,
which I (still) argue is incorrect.  As I said last time and this time,
I'd be happy to insert a "drift" term, but lets please label it what it
is.  If you agree with that, I'll make that change and push.

-- 
Wes Hardaker
USC/ISI

---

From: Wes Hardaker <[email-masked]>
Date: Tue, 12 Dec 2017 10:01:57 -0800

Paul Vixie <[email-masked]> writes:

> This timing based approach to online DNSSEC signing key changes is
> subtle beyond anybody's expectations, and because it will be used by
> the root zone, it is vital that we do more than simply whiteboard our
> proposed methods.

I have a thought about a demonstration. Will try to work on it later today.
-- 
Wes Hardaker
USC/ISI

---

From: Michael StJohns <[email-masked]>
Date: Tue, 12 Dec 2017 14:34:38 -0500

> And the last query from the last resolver will be at T + activeRefresh +

Seriously no.

This isn't this hard.  You need to stop thinking about what's happening 
from the point of view of one client and think about how the server 
views the behavior of the collection of clients.

Dealing with your "attack" scenario and assuming no retransmissions for 
any client before it gets a response to its first query, the earliest 
time that the server can assume that the first client can start its 
addHoldDown timer is right after T (the lastExpirationTime).   The 
latest time that the server can assume that the last client will start 
its addHoldDown timer is the activeRefresh interval after T.

(Assume a queryInterval of 14 hours and a set of 840 clients evenly 
distributed with their refreshes happening one a minute - the last 
client (#840) will make its query at T + activeRefresh and start its 
addHoldDown timer then).

So dealing only with that last client the server has to wait at least T 
+ activeRefresh before it assumes that the client has started its 
addHoldDown and T + activeRefresh + addHoldDown before the client has 
finished its addHoldDown and is about to make its last query.

The best case scenario (from the servers point of view) is when that 
same client has its last query before its addHoldDown time expires at 
just under the activeRefresh interval (e.g. if the expiration is at 
noon, and the active refresh is 1 hour, then the best case if the last 
query was at 11:00:00.00001) causing the query after the addHoldDown 
time to occur at 12:00:00.00001.   The worst case scenario is when ANY 
client has its last query at .00001 before the addHoldDown time expires, 
making the final query happen at the activeRefresh interval after the 
expiration or in the example at 12:59.59.99999.

For a given client assuming no query losses, there are  FLOOR 
(addHoldDown/activeRefresh) queries in the addHoldDown interval (between 
when the client starts its timer and when it goes off). The difference 
addHoldDown - (FLOOR (addHoldDown/activeRefresh) * activeRefresh) is 
this activeRefreshOffset interval you keep trying to put in.  However, 
we do assume losses and we do (and MUST) assume the worst case that at 
least one client out of 10K, 100K or 1M is going to end up doing fast 
queries and changing that difference such that they end up with their 
last query before their addHoldDown timer occurs JUST before it expires.

> or concepts within it to get it right.

And here's where you go off the rails.   You don't need to include the 
safety factor to deal with the (2) to (3) interval as retransmit can't 
reduce the addHoldDown period, but can reduce - for a given client - the 
number of queries in the period.  Retransmits and drift also can change 
*when* in that interval the given client produces its last query before 
expiration - e.g. cause a "phase shift".

> is.  If you agree with that, I'll make that change and push.

*sigh*

No.   You screw up the analysis doing it that way because it doesn't 
account for the possible "phase shift" that can happen with a client 
during its addHoldDown period.

I've been using "sigExpire + activeRefresh + addHoldDown + activeRefresh 
+ safetyFactor" because its actually easier to calculate.  The actual 
formula is

"sigExpire + (activeRefresh + retransSlop) + addHoldDown + 
(activeRefresh + retransSlop)"  where retransSlop is about 1/2 the 
safety factor.

Both the interval after sigExpire and before addHoldDown and the 
interval after the last activeRefresh require a safety factor to account 
for retransmissions.   The addHoldDown interval does not because retrans 
and drift result in a phase shift within the interval, but do not affect 
the length of the total interval.

A "perfect" system will behave the way you've described - but adding a 
safety factor while ignoring the phase shift brought on by retransmits 
within the addHoldDown interval will not characterize the actual system.

I hope this is visible.  The first group is the "perfect" one where we 
start at random point in the interval [0..activeRefresh].  The second 
group is the one with retransmits inside the add Hold down and where the 
signature expired just after the last refresh.  The third is Wes' 
perfect with the queries starting just as the sigPeriod expires.  Wes 
would add the saftety factor at the end of the third one and call it 
done.  The second one represents the actual worst case to which we'd add 
the safetyFactor to account for drift and retransmits for the two 
activeRefresh intervals.

Can we stop now?

Mike

---

From: Wes Hardaker <[email-masked]>
Date: Tue, 12 Dec 2017 13:03:47 -0800

> within the addHoldDown interval will not characterize the actual

Ah ha!  So, you do actually agree that my description of the perfect
case is true, which means we really have been in violent agreement about
the "perfect" line in the sand.  [as I've said: I absolutely understand
the point your making about real world issues, such as timing drifts]

And as I said in the last message, I agreed that a delay/phase-shift
made sense and I'd be happy to produce a new term for it.  Ideally I'd
like to add that into the safetyMargin because it reflects real-world
conditions, and I was trying to contain that to just one particular
term.  But I understand you don't want it buried in there, so I'll
create a new term to deal with timing phase shifts and add that before
the existing safetyMargin.

> Can we stop now?

I think so as I believe I can add words that we'll both agree to.  But
I've said that before, so...

The only remaining questions, just to double double be sure:

1) "is one activeRefresh period long enough to account for the slop
   associated with time clock drifts?"

   I'd argue yes, as any clock that drifted longer than an activeRefresh
   (min = 1hr) is a seriously broken clock.  I think you agree.

2) "is one activeRefresh period long enough to account for network
   delays and other elements, aside from 'retries and missing queries'?"

   I think you and I agree on this too, that one should be sufficient to
   cover network delays too.

-- 
Wes Hardaker
USC/ISI

---

From: Michael StJohns <[email-masked]>
Date: Tue, 12 Dec 2017 17:20:51 -0500

> the point your making about real world issues, such as timing drifts]

This is what you got out of all of that?   What I said was that doing 
any analysis starting from the "perfect" model would not lead you to the 
right place.  Your description of the perfect case is correct in its 
behavior and totally irrelevant to figuring out the final answer.

> conditions,
No - it doesn't.  Please look at the diagrams I provided and think about 
them for a day before responding.    I think you're still confusing 
system behavior with client behavior.

> and I was trying to contain that to just one particular
No - please don't. Please use the math I gave you.  It is correct.

Here's the whole diagram including retransmits in all the possible 
places and assuming a best case start compared against a "perfect" model 
with the same best case start:

The only effect of the retransmissions within the addHoldDown time is to 
shift when the final query happens.  In a pool of 10K clients, you'll 
get a percentage of those clients taking an entire activeRefresh 
interval after THEIR addHoldDown expires.

In no case does any calculation that involves an "activeRefreshOffset" 
provide a worst case analysis.
In no case does a "phaseShift" term provide any value over assuming that 
the addHoldDown expires just after the last query in the 
addHoldDownInterval.

So again:

sigExpiration + activeRefresh + addHoldDown + activeRefresh + 
retransmission slop/aka safetyFactor.

>     (min = 1hr) is a seriously broken clock.  I think you agree.
I don't know why you think this matters.  Assume that drift + 
retransmissions is enough to get the phase shifted to the worst case 
(e.g. just before the end of the addHoldDown period for at least one 
client) and we're done with this analysis.

>     cover network delays too.
Again, I don't know why you think this matters.   If this is the safety 
factor, then no - you're asking the wrong question.  If this is just 
about accounting for the actual time to accomplish a query then you only 
have to account for the round trip delays for the query before the 
addHoldDown and the round trip delays for the query after.  Any delays 
during the addHoldDown time only cause the phase to shift.  Those delays 
are more than covered by a single fastQuery interval and can be ignored 
with any reasonable safetyFactor.

For the safetyFactor you want to consider how large the pool of queriers 
and how often the queries fail to figure out how big to make the safety 
factor.  This ends up looking like a cumulative distribution function 
and you're looking to pick a cutoff far enough along the curve that you 
get most (tm) of the clients having completed the installation before 
moving on. 
https://en.wikipedia.org/wiki/Binomial_distribution#/media/File:Binomial_distribution_cdf.svg 
for example.

Later, Mike

---

From: Michael StJohns <[email-masked]>
Date: Thu, 14 Dec 2017 22:26:13 -0500

Below is a java program I wrote to model this stuff.  In the table, SF2 
represents the number of clients that blew past twice the safety factor 
(for aR+aHD+aR), SF1 represents the number of clients that blew past the 
single safety factor.  OF is the number of clients using the 
activeRefreshOffset calculation that finished after the calculated 
interval (e.g. aR+aHD+aRO).  OF+s is the number of clients that finished 
after the activeRefreshOffset + safetyFactor (in the first table these 
are the same because of perfect responses).   In the second table, 
compare SF1 to OF+s - SF1 < OF+s suggesting that activeRefresh is a 
better choice that activeRefreshQuery for the third term of the 
equation.  You can try a lot of different combinations, but I haven't 
found any case where OF+s performs better that SF1.

The difference between lastStart and lAddHoldBegin represents the 
retransmits after the first query.  The differences between lAddHoldEnd 
and lFinalQuery represent retransmits after the last normal query before 
the end of the add hold down time until a valid answer was received 
after the addHoldDown time expired.

Feel free to twiddle with this.

In this table, almost 1/5th of each of the clients exceeds the 
activeRefreshOffset based calculation. Note that the safety factor 
doesn't come into play as this specifies a "0" query failure rate.
> java Test5011
> Original TTL? (enter dd:hh:mm:ss) 1:4:0:0
> New TTL? (enter dd:hh:mm:ss) 1:4:0:0
> RRSig expiration interval? (enter dd:hh:mm:ss) 7:0:0:0
>
> Orig TTL: 100800 seconds
> New TTL: 100800 seconds
> RRSig interval: 604800 seconds
> QueryInterval: 50400 seconds
> FastQuery: 8640 seconds
> AddHoldDown: 2592000 seconds
> ActiveRefreshOffset: 21600 seconds
> activeRefresh + addHoldDown + activeRefresh: 2692800
>
> Number of trials(-1 to exit) 10
> Number of clients(-1 to exit) 25000
> Failure rate [0 to .99999](-1 to exit) 0
> Calculated safety factor: 0 (0.000000)
> aR + aHD + aR + sF: 2692800
>
> Offset calc - aR + aHD + aRO: 2664000
> Offset plus safety: 2664000
>
> Trial  LastStart  lAddHoldBegin lAddHoldEnd  sOffset lFinalQuery SF2 
> SF1  OF  OF+s
> -----------------------------------------------------------------------------------
> 0      50394      50394         2671194       28800 2671194    0    
> 0    3459  3459
> 1      50399      50399         2671199       28800 2671199    0    
> 0    3576  3576
> 2      50399      50399         2671199       28800 2671199    0    
> 0    3540  3540
> 3      50399      50399         2671199       28800 2671199    0    
> 0    3508  3508
> 4      50399      50399         2671199       28800 2671199    0    
> 0    3610  3610
> 5      50399      50399         2671199       28800 2671199    0    
> 0    3544  3544
> 6      50398      50398         2671198       28800 2671198    0    
> 0    3546  3546
> 7      50399      50399         2671199       28800 2671199    0    
> 0    3611  3611
> 8      50398      50398         2671198       28800 2671198    0    
> 0    3579  3579
> 9      50398      50398         2671198       28800 2671198    0    
> 0    3517  3517

> $ !-2
> java Test5011
> Original TTL? (enter dd:hh:mm:ss) 1:0:0:0
> New TTL? (enter dd:hh:mm:ss) 1:0:0:0
> RRSig expiration interval? (enter dd:hh:mm:ss) 7:0:0:0
>
> Orig TTL: 86400 seconds
> New TTL: 86400 seconds
> RRSig interval: 604800 seconds
> QueryInterval: 43200 seconds
> FastQuery: 8640 seconds
> AddHoldDown: 2592000 seconds
> ActiveRefreshOffset: 0 seconds
> activeRefresh + addHoldDown + activeRefresh: 2678400
>
> Number of trials(-1 to exit) 10
> Number of clients(-1 to exit) 100000
> Failure rate [0 to .99999](-1 to exit) .25
> Calculated safety factor: 77760 (9.000000)
> aR + aHD + aR + sF: 2756160
>
> Offset calc - aR + aHD + aRO: 2635200
> Offset plus safety: 2712960
>
> Trial  LastStart  lAddHoldBegin lAddHoldEnd  sOffset lFinalQuery SF2 
> SF1  OF  OF+s
> -----------------------------------------------------------------------------------
> 0      43199      108405        2769525       0 2769525    0    1    
> 54772 31
> 1      43198      95039         2729831       0 2753326    0    0    
> 55168 26
> 2      43199      111816        2725266       0 2740462    0    0    
> 54697 44
> 3      43199      90159         2736412       0 2745052    0    0    
> 54882 31
> 4      43199      100917        2727795       0 2729947    0    0    
> 54869 33
> 5      43199      94115         2726425       0 2740485    0    0    
> 54978 23
> 6      43199      117523        2735443       0 2743990    0    0    
> 55306 27
> 7      43198      101417        2734743       0 2743383    0    0    
> 55006 29
> 8      43199      94634         2728554       0 2747760    0    0    
> 54943 36
> 9      43199      85218         2727853       0 2737153    0    0    
> 55070 33

import java.io.*;
import java.security.SecureRandom;

public class Test5011 {

   static  BufferedReader in = new BufferedReader(new InputStreamReader 
(System.in));

   public static void main (String[] args)
     throws Exception {

     int ttl = getResponse("Original TTL");
     int newTTL = getResponse ("New TTL");
     int rrsigExpire = getResponse ("RRSig expiration interval");
     System.out.println();

     boolean useSafetyFactor = true;

     System.out.printf ("Orig TTL: %d seconds%nNew TTL: %d 
seconds%nRRSig interval: %d seconds%n", ttl, newTTL, rrsigExpire);

     int thirtyDays = convertToSeconds ("30:0:0:0");
     int oneDay = convertToSeconds ("1:0:0:0");

     int oneHour = convertToSeconds ("0:1:0:0");

     int queryInterval = Math.max (oneHour, Math.min (thirtyDays/2, 
Math.min (ttl/2, rrsigExpire/2)));

     int fastQuery = Math.max (oneHour, Math.min (oneDay/10, Math.min 
(ttl/10, rrsigExpire/10)));
     int addHoldDown = Math.max (thirtyDays, newTTL);
     System.out.printf ("QueryInterval: %d seconds%nFastQuery: %d 
seconds%nAddHoldDown: %d seconds%n",
                queryInterval, fastQuery, addHoldDown);
     int activeRefreshOffset = addHoldDown % queryInterval;
     System.out.printf ("ActiveRefreshOffset: %d seconds%n", 
activeRefreshOffset);
     System.out.printf ("activeRefresh + addHoldDown + activeRefresh: 
%d%n%n",
                2*queryInterval + addHoldDown);

     int numberOfTrials = getInt ("Number of trials");
     int numberOfClients = getInt ("Number of clients");
     double queryFailureRate = getDouble ("Failure rate [0 to .99999]");

     double safetyFactorMultiple = Math.ceil((Math.log 
(numberOfClients)/Math.log(1.0/(queryFailureRate))));
     int origSafetyFactor = fastQuery * (int)safetyFactorMultiple;
     int safetyFactor = 2* fastQuery * (int)safetyFactorMultiple;
     int totalSafeInterval = safetyFactor + 2* queryInterval + addHoldDown;
     int totalOrigSafeInterval = origSafetyFactor + 2*queryInterval + 
addHoldDown;
     int offsetInterval = queryInterval + addHoldDown + activeRefreshOffset;
     int offsetSafeInterval = offsetInterval + origSafetyFactor;
     System.out.printf ("Calculated safety factor: %d (%.6f)%n", 
origSafetyFactor, safetyFactorMultiple);
     System.out.printf ("aR + aHD + aR + sF: %d%n%n", 
totalOrigSafeInterval);
     System.out.printf ("Offset calc - aR + aHD + aRO: %d%n", 
offsetInterval);
     System.out.printf ("Offset plus safety: %d%n%n", offsetSafeInterval);

     SecureRandom rnd = new SecureRandom();

     System.out.printf ("Trial  LastStart  lAddHoldBegin lAddHoldEnd  
sOffset  lFinalQuery SF2 SF1  OF  OF+s%n"+
"-----------------------------------------------------------------------------------%n");
     for (int j = 0; j < numberOfTrials; j++) {
     int latestStartOffset = 0;
     int latestAddHoldBegin = 0;
     int latestAddHoldEnd = 0;
     int latestFinalQuery = 0;
     int smallestOffsetAtEndOfAddHold = queryInterval;
     int numExceededSafety = 0;
     int numExceedOrigSafety = 0;
     int numExceedOffset = 0;
     int numExceedOffsetSafe = 0;

     for (int i = 0; i < numberOfClients; i++) {
     // each trial
       int startOffset = rnd.nextInt(queryInterval);
       latestStartOffset = (startOffset > latestStartOffset) ? 
startOffset : latestStartOffset;

       /* Roll the dice after each query and if we roll a number < the 
query failure rate, queue up a fast query and retry */

       while (rnd.nextDouble () < queryFailureRate) {
     startOffset += fastQuery;
       }

       latestAddHoldBegin = (startOffset > latestAddHoldBegin) ? 
startOffset : latestAddHoldBegin;

       // At beginning of add hold down period
       int trialAddHoldDown = 0;
       do {
     startOffset += queryInterval;
     trialAddHoldDown += queryInterval;
     // and retransmits
     while  (rnd.nextDouble () < queryFailureRate) {
       startOffset += fastQuery;
       trialAddHoldDown += fastQuery;
     }

       } while (trialAddHoldDown  < addHoldDown);
       latestAddHoldEnd = (startOffset > latestAddHoldEnd) ? startOffset 
: latestAddHoldEnd;
       int holddiff = trialAddHoldDown - addHoldDown;
       smallestOffsetAtEndOfAddHold = (holddiff < 
smallestOffsetAtEndOfAddHold) ?
     holddiff : smallestOffsetAtEndOfAddHold;

       // past the end of the addHoldDown, the previous last query above

       while (rnd.nextDouble () < queryFailureRate) {
     startOffset += fastQuery;
       }

       latestFinalQuery = (startOffset > latestFinalQuery) ? startOffset 
: latestFinalQuery;
       if (startOffset > totalSafeInterval)
     numExceededSafety++;
       if (startOffset >totalOrigSafeInterval)
     numExceedOrigSafety++;
       if (startOffset > offsetInterval)
     numExceedOffset++;
       if (startOffset > offsetSafeInterval)
     numExceedOffsetSafe++;

     }
     //System.out.printf ("Clients: %d%nLatestStartOffset 
%d%nLatestAddHoldBegin: %d%nLatestAddHoldEnd: %d%nSmallest Offset: 
%d%nLatestFinalQuery %d%n%n",

     System.out.printf ("%-7d%-11d%-14d%-14d%-9d%-11d%-5d%-5d%-5d %-5d%n",
                j,latestStartOffset, latestAddHoldBegin,
                 latestAddHoldEnd, smallestOffsetAtEndOfAddHold,
                latestFinalQuery, 
numExceededSafety,numExceedOrigSafety,numExceedOffset, numExceedOffsetSafe);
     }

   }

    public static double getDouble (String prompt)
     throws Exception {
     do {
       System.out.printf ("%s(-1 to exit) ", prompt);
       String resp = in.readLine();
       try {
     double val = Double.valueOf(resp);
     if (val < 0.0 || val >= 1.0)
       throw new NumberFormatException ("Number out of range");
     return val;
       } catch (NumberFormatException ex) {
     System.out.println (ex.getMessage());
       }
     } while (true);
   }

   public static int getInt (String prompt)
     throws Exception {
     do {
       System.out.printf ("%s(-1 to exit) ", prompt);
       String resp = in.readLine();
       try {
     int val = Integer.valueOf(resp);
     if (val < 0)
       System.exit(1);
     return val;
       } catch (NumberFormatException ex) {
     System.out.println (ex.getMessage());
       }
     } while (true);
   }

   // return interval in seconds
   public static int getResponse(String prompt)
     throws Exception{

     do {
       System.out.printf ("%s? (enter dd:hh:mm:ss) ", prompt);
       String resp = in.readLine();
       try {
     return convertToSeconds(resp);
       } catch (NumberFormatException ex) {
     System.out.println (ex.getMessage());
       }

     } while (true);
   }

   public static int convertToSeconds (String val)
     throws NumberFormatException {
     String[] comps = val.split(":");
     if (comps.length != 4) {
       throw new NumberFormatException ("Wrong number of components");
     }
     int[] conv = new int[4];

     for (int i = 0; i < 4; i++) {

       conv[i]  = Integer.valueOf(comps[i]);
       if (conv[i] < 0)
     throw new NumberFormatException("Less than zero");
     }

     if (conv[1] > 23 || conv[2] > 60 || conv[3] > 60) {
     throw new NumberFormatException ("Bad format - out of range for one 
or more elements");

     }
     return conv[3] + conv[2] * 60 + conv[1] * 3600 + conv[0] * 86400;

   }
}

---

From: Wes Hardaker <[email-masked]>
Date: Fri, 15 Dec 2017 15:55:56 -0800

Michael StJohns <[email-masked]> writes:

> Below is a java program I wrote to model this stuff.  In the table,
> SF2 represents the number of clients that blew past twice the safety
> factor (for aR+aHD+aR), SF1 represents the number of clients that blew
> past the single safety factor.  OF is the number of clients using the
> interval (e.g. aR+aHD+aRO).  OF+s is the number of clients that
> finished after the activeRefreshOffset + safetyFactor (in the first
> table these are the same because of perfect responses).   In the
> second table, compare SF1 to OF+s - SF1 < OF+s suggesting that
> activeRefresh is a better choice that activeRefreshQuery for the third
> term of the equation.  You can try a lot of different combinations,
> but I haven't found any case where OF+s performs better that SF1.
>
> retransmits after the first query.  The differences between
> lAddHoldEnd and lFinalQuery represent retransmits after the last
> normal query before the end of the add hold down time until a valid
> answer was received after the addHoldDown time expired.
>

Work bogged me down to able to write anything back so far.  Thanks for
the java code; I'll respond with the java*script* code I've been hacking
up at the same time:

https://www.isi.edu/~hardaker/projects/5011/

I didn't add the re-transmit time issue that your code takes into
account, but I did add a query drift that nicely shows one of your
concerns.  In particular, with various values of query drift (including
-1) you can reproduce the real world situation that you're worried
about, which is (as I've mentioned) an important one to call out.

-- 
Wes Hardaker
USC/ISI

---

From: Michael StJohns <[email-masked]>
Date: Sat, 16 Dec 2017 13:41:46 -0500

Hi Wes -

Interesting approach, but characterizing a single resolver isn't 
useful.  You need to characterize the entire set of resolvers doing the 
query.

Also, you're still missing the fact that a given resolver can start its 
addHoldDown timer anywhere in the range of [0..activeRefresh] AFTER the 
signature expiration depending on when it was starting its clock.  All 
of your calculations are starting from the wrong point.

You're also continuing to miss the point that a given resolver makes its 
last query (assuming no retransmits) anywhere in the range of 
[0..activeRefresh] after the addHoldDown timer expires.

Your calculations represent the best case [e.g. triggers at 0 for both 
ends of the problem] when what you want is worst case.

Below you've calculated activeRefresh as 43200

> Timestamp
>
> 	Human Time 	Note
> 0 	0d 0h 0m 0s 	Resolver Queries
> *New DNSKEY Publication*
> 604800 	7d 0h 0m 0s 	Resolver Queries
> *sigExpirationTime = Original DNSKEY RRSIG Expires*
> 604800 	7d 0h 0m 0s 	DNSKEY First seen
>
648000 7d 12h 0m 0s Latest time DNSKEY first seen. (sigExpirationTime + 
activeRefresh)
> 3196800 	37d 0h 0m 0s 	Resolver Queries
> *sigExpirationTime + addHoldDownTimer*
>
**
> 3196800 	37d 0h 0m 0s 	DNSKEY Accepted
>
*3240000 37d 12h - Add Hold down expired for last client 
sigExpirationTime + activeRefresh + addHoldDownTimer (without retransmits)
*
> 3240000 	37d 12h 0m 0s 	Resolver Queries
> *sigExpirationTime + addHoldDownTimer + activeRefresh*
> 3240000 	37d 12h 0m 0s 	*sigExpirationTime + addHoldDownTimer + 
> activeRefresh + activeRefreshOffset*
>
*3283200  38d 0m 0s  DNSKey accepted by last client sigExpirationTime + 
activeRefresh + addHoldDownTimer  (without retransmits)*
> 3326400 	38d 12h 0m 0s 	Resolver Queries
> *sigExpirationTime + addHoldDownTimer + activeRefresh + driftSafety*
> 3326400 	38d 12h 0m 0s 	*sigExpirationTime + addHoldDownTimer + 
> activeRefresh + activeRefreshOffset + driftSafety*
> 3412800 	39d 12h 0m 0s 	Resolver Queries
> *sigExpirationTime + addHoldDownTimer + activeRefresh + driftSafety + 
> retrySafety*
>

For the rest - drift safety is probably no more 10seconds per query - 
call it for this formula about 620 seconds for the worst case (not 
assuming retransmit) and could mostly be ignored - the worst case is 
going to be less than a fastQueryInterval in all but pathological 
cases.    Basically 10s * (2 * activeRefresh + addHoldDown)/activeRefresh.

RetrySafety needs to be calculated on the set of clients as we're 
looking for the worst case of all of the clients (rightmost point of the 
normal distribution curve).

I'd suggest redoing this as a simulation.  I used the two sets of params 
(30 days and 24 hours vs 30 days and  28 hours) (sig expire and ttl) 
plus .05 failure plus 25000 clients and ran multiple trials for each.  
The values in my table represent - for each trial - the latest time a 
client got to that point.  For the last four columns, those values 
represent the number of times a client exceeded the calculated safe 
interval (and divide by the total number of clients to get a 
percentage...).

Later, Mike

---

From: Wes Hardaker <[email-masked]>
Date: Sun, 17 Dec 2017 09:20:09 -0800

Michael StJohns <[email-masked]> writes:

> Also, you're still missing the fact that a given resolver can start
> its addHoldDown timer anywhere in the range of [0..activeRefresh]
> AFTER the signature expiration depending on when it was starting its
> clock.  All of your calculations are starting from the wrong point.

No I'm not.  Set the resolver query offset amount to something large and
it does exactly what you're talking about.  I didn't forget it.

In fact, the most interesting case is when you set it large (set it to
300d, and the code will clip it to activeRefresh-1 anyway) and then you
set the resolver query drift amount to -1.  You'll note, in this case,
that that the dnskey is accepted *after* my current activeRefreshOffset
line, *proving* your case.  You keep saying I don't understand and don't
agree with you, but if this doesn't prove that I both do understand and
agree with the problem I don't know what will.  Go change *only* those
two lines in the simulator and it *shows* that you have very
legitimately found a source of real-world error that must be accounted for.

>
> Your calculations represent the best case [e.g. triggers at 0 for both ends of the problem] when what
> you want is worst case.

Try 300d in resolver query offset.
-- 
Wes Hardaker
USC/ISI