# Admission Process **Responsibility.** Generic, value-free process: consume a measurement record + a policy instance → decide/log. Downstream of Measurement. Used by an admission-gate implementer. Policy *values* live outside this pack (e.g. `gl-admit-*`). **Source.** Satoshi Hike 2.3 (tag `spec-2.3.0`). Section numbers are the specification section numbers: - §15.1 Inputs - §15.2 Outputs and states - §15.3 Two-phase async - §15.4 Admission levers - §15.5 Thorough draw and the floor **FRAMING (2026-10-24).** Rule A5 unknown-kind *measurement* lives in Measurement Record; Admission only judges with reason `unknown_kind` (Rule B9). Gate/sign-in wire and groundinglab Gate deployment detail in this file are **ops-shaped**; the SPEC intent of this document is the generic consume-then-decide process. Lever *values* are policy. **Core:** a measurement record plus a policy instance → decide. This specification defines no tip-versus-last-beacon submit deadline. --- ## 15. Admission service **Normative for the admission service only. Part B.** This section defines the admission levers and the comparisons that use them. A lever is a name, a unit, an allowed range, and a measuring or comparison rule (section 15.4). A policy sets each lever to a value inside its range. This file sets no lever value and gives no lever a default. An admission service with no policy file, or whose policy file is refused, admits nothing. It has no built-in lever values. A policy is a separate document. It names the version of this file it targets, its own `policy_version`, and the value of each lever it sets. A value outside a lever’s allowed range does not conform to this file. Admission reads only a section 14 record whose `profile` is `production`, and an admission service on the live plane compares against the mainnet portable index only (Rule B9). Those are rules of this file, not levers, and no policy sets them aside. Rules B0, B2, B3, B4, B6, B8, and B9, the Rule B7 caps and fake-leg ignore, and the re-verify rule in section 15.1 **Binding** are normative. Integrity outcomes beyond Rule B9, any plant-gap comparison, and friendly-entry custody are policy, not rules of this file. A `provisional` state grants no action (`provisional_action_cap`); that is a rule of this file, not a lever. The section 15.1 sign-in wire is normative: the challenge link, `challenge_bytes`, the BIP-340 message, the origin display, the challenge window, the submit request, the client response codes, and the website outcome tokens. The signed origin is the requesting site’s own origin, compared exactly (section 15.1 **Origin**); `https://groundinglab.io` is an example deployment. That wire adds no leg-rate value and no admission bar. Section 15.5 states the thorough leg draw and the floor’s measuring method. Part A records and measures. This section is the only judgment. The producer in section 13 records evidence and does not judge. The verifier in section 14 outputs a verify record of measurements and does not judge. The admission service applies this section. The admission service is the actor in this section. Any service that meets this section conforms to it. The groundinglab Admission Gate (Gate) is one implementation, run for the example deployment. It is not a second specification. **FRAMING:** Gate/sign-in service detail in this section is ops-shaped; treat the pack intent of Admission Process as value-free consume-measurement-then-decide, with policy instances outside. The signing client uses one route, `POST /v1/sign-in`, at the default door named in section 15.1. `POST /v1/sign-in/challenge`, `POST /v1/challenge`, and `POST /v1/admit` are not routes. The website mints the challenge link and owns the browser session. That session is not a field of the hike object and not a field of the verify record. The grant named later in this section is not placed in the link and is not returned to the signing client. A deployment of the admission service may spell `refuse` as `deny` on its wire. The word in this file is `refuse`. A threshold here can change without changing section 13 and without changing the measurements in section 14. No order among the admission rules in this section is fixed. The Gate decision log is the companion specification Gate decision log, outside this file. This file does not write that log and does not write log JSON. Thorough outcomes are: thorough decides `accept` and the accept stands; `revoke`; `refuse`; or `held`. `held` is a thorough `not read` or a retry. It is not a negative settle. A negative thorough outcome, or a failed policy-version re-verify, is `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. `provisional` then `held` then a negative thorough outcome is `revoke`. A later `revoke` cites a new policy version or a new measurement. Admission outcomes inside this section are `provisional`, `accept`, `refuse`, `revoke`, and `held`. `held` is an admission outcome. It is not a decision-log event. Section 15.1 names the signing client's response and the website outcome. ### 15.1 Inputs **Normative.** The admission service runs its own conforming section 14 verifier on the hike object it receives. It never takes a verify record supplied by the submitter. The record it produces is bound to the object hash: `SHA-256` of the exact object bytes. The inputs are: 1. The hike object, its bytes. 2. The object’s `public_key`. 3. The object hash, `SHA-256` of those bytes. 4. The challenge defined in this section. The website mints it as the link in this section. 5. A BIP-340 signature over that challenge, checked against `public_key`. Produce and verify do not check a signature (section 2). The signature check is not a section 14 measurement. 6. A policy profile (section 15.4). The profile may name a friendly list. The friendly list is not a field of the hike object and not a field of the verify record. #### Challenge The Sign-in door accepts only the challenge this section defines. It does not accept a signature over a display-order index header hash or any other challenge layout. **Domain.** The domain is the 19 ASCII bytes `pcoc-gate-signin-v1`. There is no trailing NUL in the tag. **Origin.** `origin` is the origin of the requesting site: the site that minted the challenge link and that the signature is for. It is the ASCII serialization of a web origin under RFC 6454 section 6.2: the lowercase scheme `https`, then `://`, then the host in lowercase (an internationalized host in its A-label form), then `:` and the port in shortest decimal only when the port is not 443. It has no userinfo, no path, no query, no fragment, and no trailing slash. `n` is its byte length, from 1 through 255. Comparison is exact. Two origins are the same only when their serialized bytes are equal. Nothing is case-folded, trimmed, decoded, or completed with a default port before the comparison. - A signing client MUST take `origin` from the scheme, host, and port of the challenge link it signs, serialized as above. It MUST NOT substitute a built-in value. It MUST NOT sign when the link's origin is not in serialized form. - Any operator MAY run an admission service. An admission service MUST accept a signed `origin` only when it is byte-for-byte equal to an origin that service is built or set to serve. It MUST NOT accept any other value. It MUST NOT normalize the received value first. - A received `origin` that is not equal to a served origin is check-order step 2: client `malformed`, unchanged from the check order below. `https://groundinglab.io` is one example deployment. It is not a requirement and is not special: for it, `n` is 23. `https://www.groundinglab.io`, `https://groundinglab.io/`, `http://groundinglab.io`, and `https://groundinglab.io:443` are not equal to it. The door host is not the Sign-in origin. Vectors: `spec/conformance/pack/sign-in-origins.json` signs the same challenge for `https://example.com` and for near misses, and gives what a service that serves only `https://example.com` returns for each. **Other fields.** `nonce` is 32 bytes. The website draws them from a CSPRNG when it mints the link. `expiry_unix_s` is a `u64` count of Unix seconds, not milliseconds. `object_sha256` is the 32-byte SHA-256 of the exact object bytes that are submitted. The preimage does not carry an IP address, a Cloudflare token, a session cookie, a user-agent, or a second copy of `public_key`. **Lifetime and clock skew.** The lifetime is the lever `challenge_lifetime_seconds` (section 15.4). `expiry_unix_s = issue_unix_s + challenge_lifetime_seconds`. `issue_unix_s` is the integer Unix second at which the website mints the link, the floor of the millisecond clock divided by 1000. The clock-skew allowance is the lever `challenge_clock_skew_seconds` (section 15.4). The lifetime is not part of the hash formula. The Gate compares the integers with a wide sum so the addition does not wrap. The Gate accepts the challenge when `now_unix_s < expiry_unix_s + challenge_clock_skew_seconds`, `expiry_unix_s <= now_unix_s + challenge_lifetime_seconds + challenge_clock_skew_seconds`, and `expiry_unix_s − challenge_lifetime_seconds ≥ nonce_store_start_unix_s + challenge_clock_skew_seconds`. `nonce_store_start_unix_s` is the integer Unix second, on the Gate’s clock, at which the current nonce-row store started: the latest time at which the store that the service’s nonce compare-and-set uses could have lost rows, by a restart, a flush, a failover, or any other loss. Every process answering the door uses that one value, read from or with that store; a process does not substitute its own start. A single-process Gate that holds the nonce rows in its own memory uses its own process start. The third comparison refuses every challenge issued before the current store start, so a challenge issued before the store could have lost rows is never valid after it. Otherwise the client code is `malformed`. There is no `expired` code. The signing client does not enforce the window. There is no grace after expiry. **Bytes.** Integers are big-endian. ```text challenge_bytes = ASCII "pcoc-gate-signin-v1" ‖ 0x00 ‖ uint8(n) ‖ origin_utf8[0 .. n) ‖ nonce[32] ‖ uint64_be(expiry_unix_s) ‖ object_sha256[32] ``` The length is `93 + n` bytes. Examples: `https://groundinglab.io` gives `n = 23` and 116 bytes; `https://example.com` gives `n = 19` and 112 bytes. **BIP-340 message.** ```text bip340_message_32 = SHA-256(challenge_bytes) ``` That 32-byte digest is the BIP-340 message. The signer must not hash it again. `SHA-256(bip340_message_32)` is not the message. The signature is 64 bytes under the object's x-only `public_key`. Verify is `bip340_verify(public_key, bip340_message_32, signature)`. **Challenge link.** One canonical form. The Android QR encodes that same UTF-8 string, with no byte-order mark. The link starts with the requesting site's origin, exactly as serialized under **Origin**; `groundinglab.io` is the example deployment's host. The path is `/sign-in/` with the trailing slash. The query order is fixed. There is no extra parameter, no fragment, no userinfo, and no port other than one that is part of the origin. The link does not name the door. The link does not name an object. ```text /sign-in/?v=1&nonce=<64 lowercase hex>&exp= ``` Example deployment: `https://groundinglab.io/sign-in/?v=1&nonce=<64 lowercase hex>&exp=`. `v` stays `1`. It names the challenge-bytes domain `pcoc-gate-signin-v1`. The object hash is not a link parameter. The client binds `object_sha256` from the hike object it chooses. `nonce` is 64 lowercase hex digits of the 32 nonce bytes. `exp` is `expiry_unix_s` in the shortest decimal form: no sign, and no leading zero except the value 0. A paste trims only leading and trailing ASCII whitespace: space, tab, CR, and LF. The remainder equals that string. Uppercase hex, `http`, an origin that is not in serialized form, an extra parameter, a parameter named `object`, a swapped parameter order, a fragment, a bare 64-hex string, a leading zero on `exp`, an `exp` that is not a `u64`, and a leading U+00A0 are not links. The client stops before any POST. The website does not emit `object=`. A link that carries `object=` is not a link. Clients do not accept it. The form `/sign-in/?h=` plus 64 hex digits is not a link form. The link classes in `sign-in.json` are given for the example origin `https://groundinglab.io`; there, `www.groundinglab.io` is another origin, not that site's link. A signature over a display-order index header hash is not this wire. **Origin display.** Before the signature is produced, the client shows the origin taken from the link (example deployment: `https://groundinglab.io`). It shows no path, no query, and no door host. On Android that origin is the only confirmation. The client then signs and POSTs, with no second control. On desktop the sign control stays disabled until the origin is shown. One press signs and POSTs once. The client chooses the hike object. That object is the current or selected produced object on the device. When the device holds several, the client lets the user pick. The chosen object's SHA-256 is `object_sha256` in `challenge_bytes` and in the POST body. When the device holds none, the client does not sign and does not POST. That stop is local. It is not a Gate token. **Submit.** Each signing client is built for the deployments it serves. The Sign-in door URL for each origin the client accepts is part of the client's build configuration. A client MUST NOT derive or guess a door URL from the origin. A client MUST refuse a link whose origin has no configured door; it does not sign and does not POST. Every signing client POSTs once to the configured Sign-in door for the link's origin. In the example deployment that door is `POST https://admission.groundinglab.io/v1/sign-in`. The content type is `application/json`. The client does not follow a redirect. The POST target is that door. It is not the scanned URL. The body is one JSON object with each of these keys once: Example deployment: ```json {"origin":"https://groundinglab.io","nonce":"<64 lowercase hex>","exp":,"object":"<64 lowercase hex>","object_b64":"","signature":"<128 lowercase hex>"} ``` `exp` is a JSON integer in the `u64` range, not a string. `object_b64` is standard base64 of the exact object bytes, with `=` padding and with no whitespace. Any other key, a missing key, a repeated key, `exp` as a string, or uppercase hex is `malformed`. The Gate rebuilds `challenge_bytes` from `origin`, `nonce`, `exp`, and `object`. The signature origin is accepted only under **Origin**. The origin is the body field. It is not the caller's `Origin` header. **Client response.** The client class is one of three. These sentences are the only sign-in sentences the client shows: `Sign-in submitted`, `bad signature`, and `malformed`. - `bad_signature`. HTTP 400. The body is `{"error":{"code":"bad_signature"}}` and no other field. The request was not submitted. The client shows `bad signature`. - `malformed`. HTTP 400. The body is `{"error":{"code":"malformed"}}` and no other field. The request was not submitted. The client shows `malformed`. - Delivery OK. Any other completed HTTP response, including 200, 413, 415, 429, and 503, except a redirect. The client shows `Sign-in submitted`. On the check-order success path the body is `{"delivery":"ok"}` and no other field. The POST body does not carry `outcome`. The client ignores `decision`, `reason`, `reasons`, `outcome`, and `verifier_ran`. A redirect (HTTP 3xx) is not followed and is not delivery OK. An HTTP 400 whose body is not one of the two objects above is not delivery OK. For a redirect, for that other HTTP 400, and when no HTTP response arrives, the client does not show `Sign-in submitted` and does not show a Gate outcome. The client does not poll. The client does not call the result read. The client does not show `accept`, `refuse`, `provisional`, `held`, `revoke`, or a policy sentence. `unknown_challenge`, `expired`, and `reused` are not client codes. A BIP-340 failure is an error named `bad_signature`. It is not a section 15 decision and it is not a fast final `refuse`. There is no decision line. An append-only hash-chained log entry may record it. This version has no signed head and no signed receipt. This file does not write the entry. **Website outcome.** The website reads `GET /v1/sign-in/result/<64 lowercase hex nonce>`. The response is HTTP 200 and the body is `{"outcome":""}` with no other field. An unknown nonce, and a nonce the Gate has not stored, is `pending`. That read does not create a row. The signing client does not call this route. The website of the requesting origin is the reader (example deployment: `https://groundinglab.io`). | `outcome` | Words on the website | |---|---| | `pending` | Progress. The word pending is not the result | | `accepted` | Open `/welcome/`. The website mints the browser session. This token is not a hike grade | | `malformed` | malformed | | `bad_signature` | bad signature | | `too_short` | too short | | `rejected` | rejected | | `unknown_kind` | unknown kind | | `too_few_waypoints_anchored` | too few waypoints anchored | `rejected` has no extra field. `too_short` is a decoded object shorter than 32 bytes. Those bytes cannot hold an x-only key, so no signature check runs. The POST is delivery OK. The signing client shows `Sign-in submitted`. `unknown_kind` is an object whose kind text is not `KIND`, after a valid signature. That outcome is not a fake-leg ignore. `accepted` is the thorough accept. While admission is unsettled, including a thorough `not read`, the website outcome stays `pending`. An unfinished page wait is not `rejected`. The website session lifetime is the website's own lifetime. It is not this challenge lifetime. `too_few_waypoints_anchored` is the words for the already-written refuse-at-once: fewer than 6 of `W_1` through `W_16` could be anchored, wherever those waypoints sit. A build that does not enforce that row does not emit the phrase. An object of `KIND` that the Gate refuses, when the refusal has no phrase in the table, is `rejected`. A later version of this file might add a plain phrase to the table. Until the phrase is in the table, the website outcome is `rejected`. The leg-rate floor the deployed policy sets stays enforced. The website and any public reason string do not show the fraction, a threshold, a verifier note, or a retry instruction. This table adds no leg-rate value and no admission bar. **Check order.** The door applies these steps in order and returns on the first step that decides. 1. The body is not one JSON object, or `nonce` is not 64 lowercase hex digits. The client code is `malformed`. There is no result row. The nonce is not consumed. 2. `origin` is not byte-for-byte equal to an origin this service serves (see **Origin**), a field breaks the **Submit** grammar, or the window does not accept the challenge. The client code is `malformed`. The nonce is not consumed. When the object bytes hash to `object` and this nonce has no consumed row, the website outcome is `malformed`. Otherwise the website outcome stays as it was. 3. SHA-256 of the object bytes is not `object`. The client code is `bad_signature`. The body does not say which check missed. The nonce is not consumed. The website outcome stays as it was. 4. A consumed row exists for this nonce. The same `object` and the same signature return the original client class and the original website outcome. Policy does not run again. Any other object or signature is client `malformed`, and the stored website outcome stays. A `submission_sha256` of the same submission returns the original outcome and does not mint a grant. 5. The object is shorter than 32 bytes. The website outcome is `too_short`. The client class is delivery OK. The nonce is consumed. 6. The bytes yield no 32-byte public key, or the signature does not verify over `bip340_message_32`. The client code is `bad_signature`. The body does not say which check missed. The nonce is not consumed. The website outcome is `bad_signature`. A later consuming submit replaces this recorded outcome. 7. The public key is on this gate’s persistent fake-leg ignore record. The website outcome is `rejected`. The client class is delivery OK. The nonce is consumed. Section 14 does not run. There is no new draw. The same refusal is returned for every later submission from that key at this gate. 8. The object is Rule A5 under the section 7.3 order: one well-formed CBOR map whose `kind` text is not `KIND`. The website outcome is `unknown_kind`. The client class is delivery OK. The nonce is consumed. This step does not add the key to the fake-leg ignore record. It does not fill terrain. 9. The object does not decode as a hike object: a section 7.3 decode failure (one well-formed data item that is not a map, or a map with no `kind` text). The website outcome is `rejected`. The client class is delivery OK. The nonce is consumed. This step does not add the key to the fake-leg ignore record. 10. The object is a hike object of `KIND`, or a malformed CBOR value (section 7.3), which section 14 records as shape `mismatch` (Rule B9). The client class is delivery OK. The nonce is consumed when this step starts. The website outcome is `pending` until policy settles, then `accepted`, or `too_few_waypoints_anchored` when that refusal is the one this section names, or `rejected`. A later policy refusal does not free the nonce. A cheap-check failure on this object is a plain refusal and does not add the key to the fake-leg ignore record. **Transport edge.** Before the check order, the door may refuse a request as oversize, as the wrong content type, or as over its transport rate. Each of those is a completed HTTP response, and the client class is delivery OK. The wrong content type writes no website outcome. Oversize and the transport rate write website `rejected`, and do not consume the nonce, only when the body is the JSON object, `nonce` is 64 lowercase hex digits, the nonce has no consumed row, and the object bytes hash to `object`. Otherwise the website outcome stays as it was. An unavailable door writes no website outcome. This rule does not change the door's transport figures and does not put those figures on the sign-in wire. **Single-use.** The service stores a row keyed by `nonce`: `origin`, `expiry_unix_s`, `object_sha256`, the signature, the client class, and the website outcome. The row is written when the nonce is consumed. Two racing submits of one fresh nonce must not both consume the nonce. The set is an atomic compare-and-set. The nonce compare-and-set does not admit the provisional grant. A result row stays readable for `result_retention_seconds` (section 15.4) after the website outcome becomes something other than `pending`. Within one nonce-row store, whatever `result_retention_seconds` is, a consumed-nonce row is kept at least until `now_unix_s ≥ expiry_unix_s + challenge_clock_skew_seconds`, so a replay while the window still accepts the challenge finds that row (check-order step 4). After that time the window check (step 2) answers a replay `malformed`. A `pending` row stays until it settles or the store loses it. No row needs to survive a restart or a flush of the store. The store may lose every row, because the window refuses every challenge issued before the current store start (**Lifetime and clock skew**): a replay, after the store lost rows, of a submission consumed before that is client `malformed` at check-order step 2, consumes nothing, and runs no policy. The user scans a new challenge. After the row is gone, the result read is `pending` and creates no row. A late replay whose window has closed is `malformed`. ```text submission_sha256 = SHA-256( object_sha256 ‖ public_key ‖ bip340_message_32 ‖ signature) ``` That preimage is 32 ‖ 32 ‖ 32 ‖ 64 = 160 bytes. The Gate decision log names how a submission is logged. This file does not write its JSON. A negative thorough outcome in that log, or a failed policy-version re-verify, is `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. A later `revoke` cites a new policy version or a new measurement. `verify_cap_per_door_session` counts verifies at this door. It is not a cap on a website session. A `provisional` state grants no action (`provisional_action_cap`). No irreversible action is allowed while `provisional`, in every profile. That value is fixed in this file. It is not a profile knob. A `provisional` grant is short, read-only access until thorough settles or `async_catch_budget` is hit. The friendly-list entry is added only at a thorough `accept`. **Informative.** EXAMPLE, not production. Origin `https://groundinglab.io`, nonce `0123456789abcdef` repeated to 32 bytes, `expiry_unix_s` `1893456000`, `object_sha256` 32 bytes of `0xaa`. That `object_sha256` is the 32-byte field the client binds from the object it chooses. It is not a link parameter. It is not the hash of a file of 32 `0xaa` bytes. The link is `https://groundinglab.io/sign-in/?v=1&nonce=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef&exp=1893456000`. `challenge_bytes` hex: ```text 70636f632d676174652d7369676e696e2d7631001768747470733a2f2f67726f756e64696e676c61622e696f0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0000000070dbd880aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` `bip340_message_32` hex: `90e38d7538d543aff5148318b7cb310595def390dac199c99bb10bc9ead5e0c2`. The vector `spec/conformance/pack/sign-in.json` signs that digest once under the fixed test key, with 32 aux bytes of zero. SHA-256 of the digest does not verify. The example expiry lies outside a window near 2026, so a live-clock submit of this example is `malformed` on the window. The test key is not a production key. #### Friendly list A friendly-list entry is added only at a thorough `accept`. A friendly list maps a `public_key` to an accepted `object_sha256` and a policy version. A listed key may sign in again only for that same already-accepted object hash. While a later-policy re-verify is `not read`, including after either reverify budget is hit, signature-only sign-in continues on the earlier accept. Only a re-verify that refuses, other than a B7 cap refuse, ends that sign-in as a `revoke`, and it removes the friendly entry only when this object is the key's current friendly entry. That sign-in does not run section 14 again. A new object hash is a new section 15.1 submission. When the policy version changes, the Gate re-verifies the accepted object. The Gate keeps those object bytes. The decision-log companion publishes every decided object. If those bytes are missing when a re-verify is needed, that is not `not read`. The next sign-in requires a full section 15.1 submission of the object. It is not a signature-only return treated as `not read`. One entry per key, and that entry is one accepted object. Which accepted object holds the entry when a key has more than one is friendly-entry custody, set by the policy in force; this file does not define it. A re-verify of an older object does not displace the current entry. A logged manual operator strike of the key is the other removal. A `revoke` in the object's own admission cycle, from `provisional` or from `held`, when thorough falls short of policy, ends that pair's grant and any signature-only sign-in for the object. Signature-only sign-in requires state `accepted`. Who edits the list by hand is a deployment concern. The list has no time expiry. **Normative.** Re-verify is the re-check rule of this section. An accept stands. A fake-leg ignore is not cleared by a policy-version change and is not cleared by a restart of a gate process. “That gate” is the admission service’s persistent per-key record, shared by every process of that service. A re-verify of a key on that record does not draw again. A policy-version change means the Gate re-verifies. If that re-verify refuses, other than a B7 cap refuse, it is a `revoke`. A B7 cap refuse is never a re-verify refuse, never a `revoke`, and never removes a friendly entry. The key's friendly entry is removed only when this object is the key's current friendly entry, as with any `revoke`. The removal is a compare-and-remove on `object_sha256`: the Gate removes the entry only if the current entry's object hash still matches this pair. A `not read` never removes the entry and never changes sign-in state. Before either `reverify_max_attempts` or `max_held_blocks` is hit, the Gate retries within the existing reverify budget. After either budget is hit on a later-policy re-verify `not read`, the hit is not a `revoke`, does not remove the friendly entry, and does not change sign-in state. After the hit, the Gate keeps retrying on each later `{index-url}` tip advance: at most one retry per strict increase in that tip height. A jump of several blocks is one retry. An equal or lower tip, including a reorg, is not a retry. These post-budget retries do not restart the spent budget and do not re-arm `held_bound`. The number of such retries is unbounded. #### Binding The fast decision and a later `revoke` bind the same submission: the object bytes, the object hash, the challenge, and the signature. This file does not mint a website session and does not add a session field to the hike object or the verify record. The grant defined above is the only session handle this section names. A `provisional` grant is short, read-only access until thorough settles or `async_catch_budget` is hit. The friendly-list entry is added only at a thorough `accept`. ### 15.2 Outputs and states The HTTP response outcomes are `provisional`, `accept`, `refuse`, `revoke`, and `held`. Those words are admission outcomes. The signing client does not receive them. Section 15.1 is the client response and the website outcome. **States** are `received`, `provisional`, `accepted`, `refused`, `revoked`, and `held`. `accept` is the thorough outcome whose state is `accepted`. The fast decision token is `provisional`. A fast-phase `accept` is invalid. The fast phase never issues `accept`. It may issue a final `refuse` when a fast rule fails on the object's own bytes: decode (the object bytes fail to decode as a hike object), waypoint rehash, shape `mismatch`, including `shape_item` `height_range`, seed-value `mismatch`, a plant or header `mismatch` confirmed by a fresh read from `{index-url}` that bypasses the local cache, an object-settled refuse-at-once, or a cap on a signed submission. Otherwise the fast phase gives `provisional`. A fast-phase `not read` is `held`. Only the thorough phase issues `accept`. `refuse` is the response whose state is `refused`. `revoke` is the response whose state is `revoked`. `held` is both a response outcome and a state. `held` is not a decision-log event. Thorough outcomes are: thorough decides `accept` and the accept stands; `revoke`; `refuse`; or `held`. `held` is a thorough `not read` or a retry. It is not a negative settle. A negative thorough outcome, or a failed policy-version re-verify, is `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. `provisional` then `held` then a negative thorough outcome is `revoke`. A later `revoke` cites a new policy version or a new measurement. Thorough decides `accept`; the accept stands. Sections 8–14 do not use these words as outcomes. This section does not say that a hike object passes or fails. This file does not write the decision-log JSON. A client cannot trigger `revoke`. `revoke_scope` is the pair `(public_key, object_sha256)`. A `revoke` binds to `(public_key, object_sha256)`. It does not end another object's grant. A `revoke` in that object's own admission cycle ends that pair's grant and any signature-only sign-in for the object. A later-policy re-verify that refuses, other than a B7 cap refuse, is a `revoke` and removes the friendly entry only when this object is the key's current friendly entry, by a compare-and-remove on `object_sha256`, only if the current entry's object hash still matches this pair. A `not read` does not. The Gate invalidates the pair's grant before it returns or logs a `revoke`. Every grant use or signature-only sign-in checks the pair's current state. How relying parties hear of a `revoke` stays a deployment concern. An admission cycle runs from `received` for a `(public_key, object_sha256)` under a `policy_version` until a thorough `accept`, or a `refuse` or `revoke` of the pair (including reason `held_bound`). A cap refuse (`verify_cap_per_object` or `verify_cap_per_door_session`) does not end the cycle. A later policy-version re-verify is not a new admission cycle and never issues `provisional`. The service keeps an atomic per-cycle record keyed on `(public_key, object_sha256, policy_version)`. That record is not the nonce row. The nonce compare-and-set stays for single-use nonces only. The record admits at most one provisional grant in the cycle. Concurrent submissions of the same pair cannot each receive one. A new submission while the cycle is open returns the current state, with no grant and no new run. A new submission while the cycle is closed `refused` or `revoked` under this `policy_version` returns that outcome and does not open a new cycle. While the pair is `accepted` and this object is the key's current friendly entry, friendly-list and signature-only rules apply. A client submission of a pair that is `accepted`, under a new `policy_version`, is a re-verify under the re-verify rule. A pair that is `refused` or `revoked` under an old `policy_version` and is submitted under a new `policy_version`, and a pair whose object no longer holds the key's friendly entry, are custody cases set by the policy in force. This file does not define them. A key on the gate’s persistent fake-leg ignore record never starts a new cycle. Row order. The new-submission rows are checked before the cap rows. A cap row applies to the submission the per-cycle record has admitted. It does not apply to a new submission of a pair whose cycle is already open. A cap hit stays in `received`. The submission response is `refuse`. It does not end the cycle. | From | When | To | |---|---|---| | `received` | A new submission of this `(public_key, object_sha256)` under this `policy_version` while the admission cycle is already open in `received`. The atomic per-cycle record is keyed on `(public_key, object_sha256, policy_version)`. It is not the nonce row. Return the current state. No grant. No new run | `received` | | `provisional` | A new submission of this pair under this `policy_version` while the admission cycle is open. Return the current state. No grant. No new run | `provisional` | | `held` | A new submission of this pair under this `policy_version` while the admission cycle is open. Return the current state. No grant. No new run | `held` | | `refused` | A new submission of this pair under this `policy_version`. The cycle is closed `refused`. Return that outcome. No new cycle | `refused` | | `revoked` | A new submission of this pair under this `policy_version`. The cycle is closed `revoked`. Return that outcome. No new cycle | `revoked` | | `accepted` | A new submission of this same pair under this `policy_version`, and this object is the key's current friendly entry. Friendly-list and signature-only rules apply. This is not a new admission cycle and it never issues `provisional` | `accepted` | | `accepted` | A client submission of a pair that is `accepted` under any `policy_version`. It never issues `provisional`. Under a new `policy_version` it is a re-verify of that pair: straight to thorough, subject to `verify_cap_per_object` on `(public_key, object_sha256, policy_version)`. The result is logged under the new version. A refuse is a `revoke` and removes the friendly entry only when this object is the key's current friendly entry, by a compare-and-remove on `object_sha256`. A `not read` keeps the entry and sign-in state. Signature-only sign-in continues. After either reverify budget is hit on a `not read`, the Gate keeps retrying on each later `{index-url}` tip advance: at most one retry per strict increase in that tip height. A jump of several blocks is one retry. An equal or lower tip, including a reorg, is not a retry. These post-budget retries do not restart the spent budget and do not re-arm `held_bound`. The number of such retries is unbounded. | `accepted` | | `received` | `provisional` is on, no grant was issued in that admission cycle, the fast measurements and the signature meet the profile, and every fast measurement the profile needs for `provisional` is present as an integer or a token other than `not read` or `not defined`. At most one provisional grant is issued in that cycle | `provisional` | | `received` | The policy in force sets `verify_cap_per_object`, and on a valid signature and a fresh challenge it is hit. A `verify_cap_per_object` refuse is final for that `submission_sha256` only. It does not change the pair's state or friendly entry. It is not the pair's thorough decision. A cap refuse (`verify_cap_per_object` or `verify_cap_per_door_session`) does not end the cycle. The pair's state does not move to `refused`. The submission response is `refuse`. Service-started re-verifies and `held` retries are not in the count. An unauthenticated rate limit is not this row. A transport rate limit is not this row | `received` | | `received` | The policy in force sets `verify_cap_per_door_session`, and on a valid signature and a fresh challenge it is hit. A cap refuse (`verify_cap_per_object` or `verify_cap_per_door_session`) does not end the cycle. The pair's state does not move to `refused`. The submission response is `refuse`. Held retries do not count. An unauthenticated rate limit is not this row. A transport rate limit is not this row | `received` | | `received` | A plant or header `mismatch` is read from the local cache and is not yet confirmed by a fresh read from `{index-url}` that bypasses the local cache | `held` | | `received` | That same plant or header `mismatch` is confirmed by a fresh read from `{index-url}` that bypasses the local cache. Invariant fast final `refuse`. No thorough run. No grant. `phase` `fast` | `refused` | | `received` | The object bytes fail to decode as a hike object. Invariant fast final `refuse`. No thorough run. No grant | `refused` | | `received` | A fast `not read` other than a six-leg read, including an unreadable header or a height or plant the mirror does not hold, when the profile needs that measurement for `provisional` | `held` | | `received` | Settled by bytes already in hand: fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored, or the section 14.6 draw is `not defined` because fewer than six legs exist. Bookend `N` `not defined` is not this row. If a re-read could bring that waypoint count to 6 or more, the measurement is `not read` and the state is `held`, and this row does not apply. A short-byte six-leg draw is not this row. `hike_span` `not read` is not this row. Bookend `S` `not read` is not this row | `refused` | | `received` | `hike_span` is `not read`. The state enters `held` and the read is retried. Bookend `S` `not read` is not this row | `held` | | `received` | Shape `mismatch`, including `shape_item` `height_range`, seed-value `mismatch`, or waypoint rehash `mismatch` is measured from the object. Waypoint rehash is the recomputed waypoint compared with the stored waypoint. Invariant fast final `refuse`. No thorough run. No grant. Not gated on a lock | `refused` | | `provisional` | A thorough `not read`, including a short or missing six-leg read, a short-byte six-leg draw, or short path bytes for a six-leg run. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The service re-verifies | `held` | | `received` | `provisional: off`. A thorough `not read`, including a short or missing six-leg read, a short-byte six-leg draw, or short path bytes for a six-leg run. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The service re-verifies | `held` | | `received` | `provisional` is on, and the fast phase has not left `received` within `fast_accept_bound`. No grant. The state enters `held`. This timeout hold does not return to `provisional` | `held` | | `received` | `provisional: off`. Thorough decides `accept`; the accept stands | `accepted` | | `received` | `provisional: off`. Thorough negative. No grant was issued | `refused` | | `provisional` | `async_catch_budget` is hit before thorough settles. The move from `provisional` to `held` ends the grant. It does not resume. No grant remains. The state enters `held`. This hold does not return to `provisional`. A later thorough refuse is `revoke`, because a grant was issued in the cycle | `held` | | `provisional` | Thorough decides `accept`; the accept stands | `accepted` | | `provisional` | Thorough negative. A grant was issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`). `phase` `thorough`. The `revoke` cites `fast_seq` | `revoked` | | `held` | `provisional` is on, no grant was issued in that admission cycle, and this hold was not entered from `fast_accept_bound` or from `async_catch_budget`. The fast inputs are read on retry | `provisional` | | `held` | This hold was entered from `fast_accept_bound` or from `async_catch_budget`. Thorough decides `accept`; the accept stands. This hold does not return to `provisional` | `accepted` | | `held` | This hold was entered from `fast_accept_bound` or from `async_catch_budget`. Thorough negative, and no grant was issued in that cycle. `phase` `thorough`. `fast_seq` is omitted. This hold does not return to `provisional` | `refused` | | `held` | This hold was entered from `fast_accept_bound` or from `async_catch_budget`. Thorough negative, and a grant was issued in the cycle. A later thorough refuse is `revoke`. `phase` `thorough`. The `revoke` cites `fast_seq`. This hold does not return to `provisional` | `revoked` | | `held` | This hold was entered from `fast_accept_bound` or from `async_catch_budget`. `reverify_max_attempts` or `max_held_blocks` is hit, whichever comes first. Same budgets, no reset. When both are hit on the same check, the detail is `reverify_max_attempts`. Reason `held_bound`. No grant was issued in that cycle. `phase` is the phase running when the budget ran out. This hold does not return to `provisional` | `refused` | | `held` | This hold was entered from `fast_accept_bound` or from `async_catch_budget`. `reverify_max_attempts` or `max_held_blocks` is hit, whichever comes first. Same budgets, no reset. When both are hit on the same check, the detail is `reverify_max_attempts`. Reason `held_bound`. A grant was issued in that cycle. `phase` `thorough`. The `revoke` cites `fast_seq`. This hold does not return to `provisional` | `revoked` | | `held` | Thorough decides `accept`; the accept stands | `accepted` | | `held` | Thorough negative, and a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`). `phase` `thorough`. The `revoke` cites `fast_seq` | `revoked` | | `held` | Thorough negative, and no grant was issued | `refused` | | `held` | A thorough `not read`, including a short or missing six-leg read, a short-byte six-leg draw, or short path bytes for a six-leg run. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. Neither `reverify_max_attempts` nor `max_held_blocks` is hit | `held` | | `held` | `reverify_max_attempts` or `max_held_blocks` is hit, whichever comes first, and a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`). Reason `held_bound`. Detail names the budget. When both budgets are hit on the same check, the detail is `reverify_max_attempts`. `phase` `thorough`. The `revoke` cites `fast_seq` | `revoked` | | `held` | No grant was issued, and the budget ran out while the fast phase was still running. Reason `held_bound`. Detail names the budget. When both budgets are hit on the same check, the detail is `reverify_max_attempts`. `phase` `fast`. No thorough line. The line carries the fast record. `fast_seq` is omitted | `refused` | | `held` | No grant was issued, and the budget ran out while the thorough phase was running. That includes `provisional: off`, and a fast-entered hold that later ran thorough when the budget hit. Reason `held_bound`. Detail names the budget. When both budgets are hit on the same check, the detail is `reverify_max_attempts`. `phase` `thorough`. `fast_seq` is omitted. It is never encoded as JSON `null`. The line carries the thorough record | `refused` | | `held` | A cache-sourced plant or header `mismatch` is confirmed by a fresh read from `{index-url}` that bypasses the local cache, and no grant was ever issued. Invariant fast final `refuse`. `phase` `fast`. No thorough run. No grant | `refused` | | `held` | A cache-sourced plant or header `mismatch` is confirmed by a fresh read from `{index-url}` that bypasses the local cache, and a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`). Invariant. `phase` `thorough`. The `revoke` cites `fast_seq`. The reason is the confirmed plant or header `mismatch` | `revoked` | | `revoked` | Any later check of that same submission | `revoked` | | `accepted` | The policy version changes. Re-verify the stored object under the new policy version. While that re-verify is `not read`, including after either reverify budget is hit, signature-only sign-in continues on the earlier accept. Thorough decides `accept`; the accept stands. The friendly entry is kept only when this object is already the key's current friendly entry. If a newer accept B later loses its accept, A is not restored. A re-verify of an older object does not displace a newer entry. This re-verify is not a new admission cycle and never issues `provisional` | `accepted` | | `accepted` | That re-verify refuses under the new policy version. The refuse is a measured `mismatch` or any B-rule outcome other than a B7 cap refuse. A `not read` is not this row. The outcome is `revoke`. `phase` `thorough`. The `revoke` cites the new policy version and `fast_seq`. The Gate invalidates the pair's grant before it returns or logs the `revoke`. When this object is the key's current friendly entry, the `revoke` removes that entry by a compare-and-remove on `object_sha256`, only if the current entry's object hash still matches this pair. A re-verify of an older object does not displace a newer entry. If a newer accept B later loses its accept, A is not restored | `revoked` | | `accepted` | That re-verify is `not read`, and neither `reverify_max_attempts` nor `max_held_blocks` counted from the first `not read` on that re-verify is hit. The earlier accept stands. No suspend. No `held_bound`. Friendly returns continue. The re-verify is retried later | `accepted` | | `accepted` | That re-verify is still `not read`, and `reverify_max_attempts` or `max_held_blocks` counted from the first `not read` on that re-verify is hit, whichever comes first. The hit is not a `revoke`, does not remove the friendly entry, and does not change sign-in state. Signature-only sign-in continues on the earlier accept. After the hit, the Gate keeps retrying on each later `{index-url}` tip advance: at most one retry per strict increase in that tip height. A jump of several blocks is one retry. An equal or lower tip, including a reorg, is not a retry. These post-budget retries do not restart the spent budget and do not re-arm `held_bound`. The number of such retries is unbounded | `accepted` | | `accepted` | The state is `accepted`. A listed key signs in again for the same already-accepted object hash. While a later-policy re-verify is `not read`, including after either reverify budget is hit, signature-only sign-in continues on the earlier accept. Only a re-verify that refuses, other than a B7 cap refuse, ends that sign-in as a `revoke`, and it removes the friendly entry only when this object is the key's current friendly entry. Section 14 does not run again. Signature-only sign-in requires state `accepted` | `accepted` | | `refused` | Any later check of that same submission | `refused` | **Normative.** Policy `refuse`s at once only when the waypoint count is settled by bytes already in hand: fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored. If a re-read could bring that count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. Bookend `N` `not defined` is not, by itself, an admission refuse. Fewer than six legs for the section 14.6 draw is `not defined` from the object's own bytes and is that same refuse-at-once. That draw is not a leg-count floor. `hike_span` `not read` stays `held`. Bookend `S` `not read` is not this hold. A fast `not read`, including an unreadable header, is `held`. A short or missing six-leg read, a short-byte six-leg draw, and short path bytes for a six-leg run are thorough-only. They are `not read`. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The state is `held` from `received` when `provisional` is off, from `provisional`, or from `held`. They are not a fast `received` to `held` row. The service re-verifies. A short-byte six-leg draw stays re-readable. A move from `provisional` to `held` ends the grant. It does not resume. No action is allowed while `held`. `held` leaves to `provisional` only when `provisional` is on, no grant was issued in that admission cycle, the hold was not entered from `fast_accept_bound` or from `async_catch_budget`, and the fast inputs are read on retry. A hold entered from `fast_accept_bound` or from `async_catch_budget` does not return to `provisional`. It leaves only to a thorough `accept`, to `refuse`, to `revoke`, or to refuse/revoke with reason `held_bound` when `reverify_max_attempts` or `max_held_blocks` runs out. Those are the same budgets, with no reset. At most one provisional grant is issued in that admission cycle. `held` leaves to `accepted` when thorough decides `accept`, to `revoked` on a negative thorough outcome when a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), or to `refused` on a negative thorough outcome when no grant was issued. `provisional` then `held` then a negative thorough outcome is `revoke`. While neither `reverify_max_attempts` nor `max_held_blocks` is hit, a thorough `not read` is retried and the state is `held`. Exhausting either budget is refuse/revoke with reason `held_bound`: `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. It does not stay `held`. That `held` is not a negative settle. `held` for a missing measurement is only for `not read`. Policy `refuse`s at once only when the waypoint count is settled by bytes already in hand: fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored. If a re-read could bring that count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. Bookend `N` `not defined` is not, by itself, an admission refuse. Fewer than six legs for the section 14.6 draw is `not defined` from the object's own bytes and is that same refuse-at-once. That draw is not a leg-count floor. `hike_span` `not read` stays `held`. Bookend `S` `not read` is not this hold. That `not defined` does not go to `held`. A fast `not read`, including an unreadable header, is `held`. A short or missing six-leg read, a short-byte six-leg draw, and short path bytes for a six-leg run are thorough-only. They are `not read`. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The state is `held` from `received` when `provisional` is off, from `provisional`, or from `held`. They are not a fast `received` to `held` row. A short-byte six-leg draw stays re-readable. A Rule A4.3 checkpoint digest that has not been recomputed does not put an honest object in `held`. A height or plant the mirror does not hold is `not read`, and the state is `held`. A plant or header `mismatch` read from that mirror is `held` until a fresh read from `{index-url}` that bypasses the local cache confirms the same comparison. `{index-url}` is the upstream. The local mirror is a cache of `{index-url}` responses. An operator's own Bitcoin-built index counts as its `{index-url}`. Anyone may still check that index against Bitcoin. Only a `mismatch` confirmed by that fresh read can `refuse`. **Normative.** A fake leg is a section 14.6 `mismatch` on a recomputed leg. The gate refuses that submission, and that gate ignores every later submission from that public key. The ignore is per key and that gate only. Its duration is the lever `fake_leg_ignore_seconds` (section 15.4); when that lever is unset the ignore has no expiry. Every later submission from that key at that gate gets the same refusal, for any object, with no new draw and no new section 14 run. A cheap-check failure, including a Rule A4.3 checkpoint-digest `mismatch`, a shape `mismatch`, a waypoint rehash `mismatch`, a seed-value `mismatch`, and an `unknown_kind` refuse, is a plain refusal. It does not add the key. The ignore record is persistent. A policy-version bump does not clear it and does not open a new cycle for that key. A gate restart does not clear it. A re-verify does not give a free redraw. A cap refuse under Rule B7 is not this ignore. The caps are not a ban. A `refuse` or a `revoke` that is not a fake leg still binds to `(public_key, object_sha256)` under that policy version. The same `submission_sha256` does not become `accepted` under that policy version. A resubmit of the same `submission_sha256` returns the original outcome. This version does not issue a signed receipt. It does not mint a grant. A new submission needs a new challenge. `held` is not a sticky `refuse` and it is not `accept`. The decision-log companion publishes the refuse. This file does not write that log. Cross-gate sharing of the fake-leg record is out of scope. Signature-only sign-in requires state `accepted`. A listed key may sign in again only for that same already-accepted object hash. While a later-policy re-verify is `not read`, including after either reverify budget is hit, signature-only sign-in continues on the earlier accept. Only a re-verify that refuses, other than a B7 cap refuse, ends that sign-in as a `revoke`, and it removes the friendly entry only when this object is the key's current friendly entry. That sign-in does not run section 14 again. A new object hash is a new section 15.1 submission. When the policy version changes, a re-verify that refuses under the new policy version revokes an `accepted` object. That refuse is a measured `mismatch` or any B-rule outcome other than a B7 cap refuse. A `not read` is not a refuse and does not revoke. That `revoke` cites the new policy version. While that re-verify is `not read` and neither `reverify_max_attempts` nor `max_held_blocks` counted from the first `not read` on that re-verify is hit, the earlier accept stands. It does not suspend the accept, it does not apply `held_bound`, and friendly returns continue. The re-verify is retried later. Both `reverify_max_attempts` and `max_held_blocks` for that re-verify are counted from the first `not read` on that re-verify. `max_held_blocks` starts at the Gate’s `{index-url}` tip height at that first `not read`. It is hit when a retry finds that tip ≥ start + `max_held_blocks` and the re-verify is still `not read`. `reverify_max_attempts` is hit when that many retries of this re-verify, counted from that first `not read`, are still `not read`. Before either budget is hit, the Gate retries within the existing reverify budget. After either budget is hit, the hit is not a `revoke`, does not remove the friendly entry, and does not change sign-in state. Signature-only sign-in continues on the earlier accept. After the hit, the Gate keeps retrying on each later `{index-url}` tip advance: at most one retry per strict increase in that tip height. A jump of several blocks is one retry. An equal or lower tip, including a reorg, is not a retry. These post-budget retries do not restart the spent budget and do not re-arm `held_bound`. The number of such retries is unbounded. A re-verify that refuses, other than a B7 cap refuse, is a `revoke` and removes the friendly entry only when this object is the key's current friendly entry. When that re-verify decides `accept`, the entry is kept only when this object is already the key's current friendly entry. If a newer accept B later loses its accept, A is not restored. A re-verify of an older object does not displace a newer entry. This file does not write that log. An index outage does not revoke that accepted object. Thorough decides `accept` and the accept stands. #### Rule B9. Invariant refuses **Normative.** These outcomes are not Rule B0–B8 candidates. They do not wait on a configured threshold. A `revoke` in this list is `phase` `thorough`. A grant means the fast phase already finished. `fast_seq` is omitted when the line does not cite one. It is never encoded as JSON `null`. - The object bytes fail to decode as a hike object. Invariant fast final `refuse`. No thorough run. No grant. `phase` `fast`. - Waypoint rehash is the recomputed waypoint compared with the stored waypoint. That `mismatch` is an invariant fast final `refuse`. No thorough run. No grant. `phase` `fast`. - Shape `mismatch`, including `shape_item` `height_range`, and seed-value `mismatch`, are invariant fast final `refuse`s. No thorough run. No grant. `phase` `fast`. - A plant or header `mismatch` confirmed by a fresh read from `{index-url}` that bypasses the local cache is an invariant. No grant was ever issued: `refuse`, `phase` `fast`, no thorough run. A grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`): `revoke`, `phase` `thorough`, and the `revoke` cites `fast_seq`. The reason is the confirmed plant or header `mismatch`. - A checkpoint-digest `mismatch` is an invariant. The thorough phase measures it. No grant was ever issued: `refuse`, `phase` `thorough`. A grant was ever issued: `revoke`, `phase` `thorough`, and the `revoke` cites `fast_seq`. It is a cheap-check failure. It does not add the public key to the fake-leg ignore record. - A six-leg comparison of `mismatch` is an invariant. The thorough phase measures it. No grant was ever issued: `refuse`, `phase` `thorough`. A grant was ever issued: `revoke`, `phase` `thorough`, and the `revoke` cites `fast_seq`. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. That is a thorough `not read`. The state is `held`. That `mismatch` is a fake leg. Rule B7 and section 15.5 then ignore later submissions of that public key at that gate. - A cap hit is not in this list. Rule B7 states it. It applies only when the policy in force sets that cap. - The admission service runs its section 14 verifier under the profile `production`, the section 6 constants, and reads only a section 14 record whose `profile` is `production`. It never runs `conformance-small` and never reads a record of any other profile. An object whose `fill` or size fields differ from section 6 is then shape `mismatch` with `shape_item` `constant` (Rule A4.1.2), an invariant fast final `refuse`. No thorough run. No grant. `phase` `fast`. An admission service on the live plane compares headers and plants against the Bitcoin mainnet portable index only: its `{index-url}` is a mainnet index. This is not a lever. - When the section 14 record’s `kind` is `unknown kind`, the admission service refuses with reason `unknown_kind`. That reason is this policy. It is not a section 14 token. Verify does not record it. No thorough run. No grant. `phase` `fast`. The key is not added to the ignore record. ### 15.3 Two-phase async 1. **Fast phase.** The fast list is the section 14.1–14.5 measurements, and the cheap checks include the Rule A4.3 checkpoint digest on every checkpoint: shape, the seed, waypoint and fold recompute from stored checkpoint bytes, path and plant comparison, bookends, and the anchored count, plus the signature. Header and plant facts come from the service's local mirror of the portable index. `{index-url}` is the upstream. The local mirror is a cache of `{index-url}` responses and serves the section 9 body rules. The mirror holds only `{index-url}` responses. It is filled from the portable index only. A height or plant the mirror does not hold is `not read`, and the state is `held`. The fast phase does no other network read. A plant or header `mismatch` read from that mirror is `held` until a fresh read from `{index-url}` that bypasses the local cache confirms the same comparison. An operator's own Bitcoin-built index counts as its `{index-url}`. Anyone may still check that index against Bitcoin. Only a `mismatch` confirmed by that fresh read can `refuse`. The fast checkpoint-binding checks are the fold and the waypoint rehash under section 12.2 from stored checkpoints. The fast phase never recomputes a leg. Leg recompute stays thorough-only. The fast phase does not turn a partial view into the finished verify record. Branch on phase. There is no new token for the phase. The Rule A4.3 checkpoint digest is a cheap check on every checkpoint. It hashes the stored sample. It does not recompute a leg. The admission service runs it with the other cheap checks. It is not thorough-only. The fast record is the section 14 record with `six_leg` and `six_leg_runs` omitted. `checkpoints[*].legs[*].result` stays, because the Rule A4.3 digest is a cheap check. Every other section 14 key stays, including `shape_item`, `checkpoints[*].g`, `checkpoints[*].legs[*].j`, `waypoints[*].result`, `seed_value`, `paths`, `anchors`, the bookend keys, and `analysis`. Omitted keys are absent. They are not filled with `not defined`. The full section 14 record, after thorough, has every key present, and `six_leg_runs` holds the thorough draws. Both records are encoded with RFC 8785. The fast record has no placeholders. It does not fill an omitted key with `not defined`. It is not the finished record. The fast decision token is `provisional`. A fast-phase `accept` is invalid. The fast phase never issues `accept`. It may issue a final `refuse` when a fast rule fails on the object's own bytes: decode (the object bytes fail to decode as a hike object), waypoint rehash, shape `mismatch`, including `shape_item` `height_range`, seed-value `mismatch`, a plant or header `mismatch` confirmed by a fresh read from `{index-url}` that bypasses the local cache, an object-settled refuse-at-once, or a cap on a signed submission. Otherwise the fast phase gives `provisional`. A fast-phase `not read` is `held`. When profile mode `provisional` is `on`, it returns within `fast_accept_bound`. That bound is a lever (section 15.4). When `provisional` is `off`, the fast phase does not grant `provisional`. `provisional: off` is that mode. It is not a value of `fast_accept_bound`. 2. **Held.** `held` for a missing measurement is only for `not read`. When a fast measurement the active profile needs for `provisional` is `not read`, the fast phase does not issue `provisional` and does not record a sticky `refuse`. The state is `held`. Policy `refuse`s at once only when the waypoint count is settled by bytes already in hand: fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored. If a re-read could bring that count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. Bookend `N` `not defined` is not, by itself, an admission refuse. Fewer than six legs for the section 14.6 draw is `not defined` from the object's own bytes and is that same refuse-at-once. That draw is not a leg-count floor. `hike_span` `not read` stays `held`. Bookend `S` `not read` is not this hold. That `not defined` does not go to `held`. A fast `not read`, including an unreadable header, is `held`. A short or missing six-leg read, a short-byte six-leg draw, and short path bytes for a six-leg run are thorough-only. They are `not read`. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The state is `held` from `received` when `provisional` is off, from `provisional`, or from `held`. They are not a fast `received` to `held` row. A short-byte six-leg draw stays re-readable. A missing Rule A4.3 checkpoint digest is not that `not defined` case: the digest is not yet run. A height or plant the mirror does not hold is `held`, as section 15.2 states. A cache-sourced plant or header `mismatch` is `held` until a fresh read from `{index-url}` that bypasses the local cache confirms it. `{index-url}` is the upstream. The local mirror is a cache of `{index-url}` responses. An operator's own Bitcoin-built index counts as its `{index-url}`. Anyone may still check that index against Bitcoin. The fast phase must not issue `provisional` on a floor that is `not read` or `not defined`. A fast-phase `not read` is `held`. A plant `mismatch` confirmed by a fresh read from `{index-url}` that bypasses the local cache is a fast final `refuse`: no thorough run and no grant. 3. **Thorough phase.** The thorough phase is the leg sample in section 15.5: `D = 6 · thorough_rounds` random legs as `thorough_rounds` rounds of 6, each round’s 6 legs run in parallel. The Rule A4.3 digest is a cheap check and is not this phase. Leg recompute (section 14.6) is thorough-only. A short or missing six-leg read, a short-byte six-leg draw, and short path bytes for a six-leg run are that thorough `not read`. When the six-leg draw is `not read`, `six_leg_runs` is `not read`. The state is `held` from `received` when `provisional` is off, from `provisional`, or from `held`. They are not a fast `received` to `held` row. Only the thorough phase issues `accept`. The fast phase never issues `accept`. The thorough phase issues `refuse` or `revoke` by the grant-ever-issued rule. A fast final `refuse` is the case named in the fast phase. Thorough outcomes are: thorough decides `accept` and the accept stands (`accepted`); `revoke`; `refuse`; or `held`. `held` is a thorough `not read` or a retry. It is not a negative settle. A negative thorough outcome, or a failed policy-version re-verify, is `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. `provisional` then `held` then a negative thorough outcome is `revoke`. A later `revoke` cites a new policy version or a new measurement. `held` is an HTTP response outcome. It is not a decision-log event. If the object was `provisional`, a move to `held` ends the grant. It does not resume. A grant was issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`). A later negative thorough outcome is `revoke`. It is `refuse` only when no grant was issued. 4. **Provisional.** The fast-phase outcome is `provisional`, a reversible grant, until thorough settles. It is never `accept`. A `provisional` state grants no action (`provisional_action_cap`). No irreversible action is allowed while `provisional`, in every profile. That value is fixed in this file. It is not a profile knob. A `provisional` grant is short, read-only access until thorough settles or `async_catch_budget` is hit. The friendly-list entry is added only at a thorough `accept`. The window before thorough settles is the attacker surface of a provisional grant. A provisional grant is not a finished admission. The residual risk is QR relay phishing: a victim scans a QR that an attacker relayed. **Informative.** Although a `provisional` state grants no action, a forged object can still reach `provisional` for free: the fast phase has no leg, and the plant gap is record-only. That grant is short, read-only access until thorough settles or `async_catch_budget` is hit. `verify_cap_per_door_session` is the main limit. **Held bound.** Normative. `reverify_max_attempts` and `max_held_blocks` are levers (section 15.4). `max_held_blocks` applies to every hold, whatever the cause: a fast `not read`, a thorough `not read`, `fast_accept_bound`, or `async_catch_budget`. It starts at the Gate’s `{index-url}` tip height at the first entry into `held` in the cycle. It does not reset. When no tip has been read at the first entry into `held`, `max_held_blocks` starts at the first successful tip read. That start tip height is the normative clock. An append-only hash-chained log entry may record that tip height as `bitcoin_tip_height`. That name is the log entry's name for the tip. The entry is unsigned. It is not a signed head. This version has no signed head and no signed receipt. This file does not define a second clock. It is hit when a check finds that tip ≥ start + `max_held_blocks`, even when no `not read` has occurred. `reverify_max_attempts` counts `not read` retries only, fast or thorough. A hold with no `not read`, including `fast_accept_bound` and `async_catch_budget`, does not count toward it. It is hit when that many counted retries have been made. A held submission is retried at least once every `held_retry_seconds` (the lever `held_retry_seconds`, section 15.4), whether or not the tip advanced. Each such retry counts under `reverify_max_attempts` by the rules in this paragraph. That timer schedules retries only and is not a clock for any budget. Whichever budget is hit first, the outcome is refuse/revoke with reason `held_bound`. The detail names the budget that hit, `reverify_max_attempts` or `max_held_blocks`. This file names that detail. It does not write the decision-log JSON. Exhausting either budget is `revoke` if a grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), and `refuse` if none was. It does not stay `held`. `phase` is the phase running when the budget ran out, and the line carries that phase's record. No grant was issued and the fast phase was still running: the outcome is `refuse`, reason `held_bound`, the detail names the budget that hit, `phase` `fast`, no thorough line, and the line carries the fast record. No grant was issued and the thorough phase was running, including `provisional: off` and a fast-entered hold that later ran thorough: the outcome is `refuse`, reason `held_bound`, the detail names the budget that hit, `phase` `thorough`, `fast_seq` is omitted, and the line carries the thorough record. `fast_seq` is omitted. It is never encoded as JSON `null`. A grant was ever issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`): the outcome is `revoke`, `phase` `thorough`, and that `revoke` cites `fast_seq`. Every `revoke` is `phase` `thorough`. When both `reverify_max_attempts` and `max_held_blocks` are hit on the same check, the detail is `reverify_max_attempts`. This file does not write the line. After a grant was issued (`provisional` or `accept`, including one already ended by the move from `provisional` to `held`), a `held_bound` outcome is `revoke`, not `refuse`. That `revoke` binds to `(public_key, object_sha256)`. It does not end another object's grant. A `revoke` in that object's own admission cycle ends that pair's grant and any signature-only sign-in for the object. A later-policy re-verify that refuses, other than a B7 cap refuse, is a `revoke` and removes the friendly entry only when this object is the key's current friendly entry, by a compare-and-remove on `object_sha256`, only if the current entry's object hash still matches this pair. A `not read` does not. Which named checks are fast and which are thorough is the profile schema in section 15.4. The fast list does not include a leg. The hit rule, the `{index-url}` tip clock, and the phase of a `held_bound` line are the Held bound paragraph above. An append-only hash-chained log entry may record that tip height as `bitcoin_tip_height`. That name is the log entry's name for the tip. The entry is unsigned. It is not a signed head. This version has no signed head and no signed receipt. That `held` is not a negative settle. The fast phase itself does not `refuse` for a `not read` gap, or for a height or plant the mirror does not hold. Policy `refuse`s at once only when the waypoint count is settled by bytes already in hand: fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored. If a re-read could bring that count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. Bookend `N` `not defined` is not, by itself, an admission refuse. Fewer than six legs for the section 14.6 draw is `not defined` from the object's own bytes and is that same refuse-at-once. That draw is not a leg-count floor. `hike_span` `not read` stays `held`. Bookend `S` `not read` is not this hold. That `not defined` does not go to `held`. A short-byte six-leg draw stays re-readable. The state is `held`. The service re-verifies. ### 15.4 Admission levers **FRAMING.** Lever *ranges* below are what a policy file may set; *values* belong in a policy instance outside this pack. **Normative.** A policy profile names its `policy_version`, the version of this file it targets, the value of each lever it sets, and its fast checks and its thorough checks. The fast list is the cheap checks: shape, header match, the Rule A4.3 checkpoint digest on every checkpoint, the seed, waypoint and fold recompute from stored checkpoints, path and plant comparison, bookends, the anchored count, and the signature. The thorough list is the section 15.5 leg sample. The fast checkpoint-binding checks are the fold and the waypoint rehash under section 12.2 from stored checkpoints. The Rule A4.3 checkpoint digest hashes the stored sample and does not recompute a leg. The fast list never recomputes a leg. Every lever is below. A lever has a name, a unit, an allowed range, and a measuring or comparison rule. A policy sets a value inside the range. This file gives no lever a default. The policy in force sets every lever. The one exception is a lever whose range lists unset: the policy may leave it absent, which means unset. “Unset” is an allowed value only where the range lists it. A lever marked gate-scoped has one value per admission service, not per profile. Integers are compared exactly, with no rounding. A value that disables a comparison is out of range. | Lever | Rule | Unit | Allowed range | Measured or compared as | |---|---|---|---|---| | `min_anchored_paths` | B4 | waypoints | integer `2 … 16` | `anchored_total` (Rule A4.5) over `W_1` … `W_16`, anywhere among them. Compared `anchored_total ≥ min_anchored_paths`. The lower bound 2 is the two anchored waypoints the bookend pair needs (Rule A4.6). `hike_span` does not read those waypoints. | | `leg_rate_floor` | B2 | checkpoints per second, as `p/q` | integers `p ≥ 1`, `q ≥ 1` | `(hike_legs, hike_span)` from Rule A4.10, `hike_span` the raw header `nTime` span in seconds, `nTime(I_17) − nTime(I_0)`. Compared `q · hike_legs ≥ p · hike_span`. Section 15.5 is the measuring method a policy uses to choose `p/q`. | | `held_retry_seconds` | 15.3 | seconds | integer `≥ 1`; gate-scoped | Wall-clock interval between retries of a held submission. A retry schedule only; not a clock for any budget. | | `reverify_max_attempts` | 15.3 | counted retries | integer `≥ 1` | Counted retries as the section 15.3 **Held bound** defines. Hit when that many have been made. | | `max_held_blocks` | 15.3 | blocks | integer `≥ 1` | `{index-url}` tip height minus the start tip of the hold, as the section 15.3 **Held bound** defines. Hit when the difference is `≥ max_held_blocks`. | | `thorough_rounds` | B7, 15.5 | rounds of 6 legs | integer `≥ 1` | The thorough draw size is `D = 6 · thorough_rounds` (section 15.5). | | `provisional` | 15.3 | mode | `on` or `off` | `off` means no `provisional` state. | | `fast_accept_bound` | 15.3 | seconds | positive; used only when `provisional` is `on` | Wall-clock time of the fast phase. | | `async_catch_budget` | 15.3 | seconds | positive | Wall-clock time of the parallel checkpoint-digest pass. It does not include recomputing legs. | | `verify_cap_per_object` | B7 | verifies per `(public_key, object_sha256, policy_version)` | integer `≥ 1`, or unset | Client-submitted verifies of that triple. Service-started re-verifies and `held` retries are not counted. | | `verify_cap_per_door_session` | B7 | client submissions per door session | integer `≥ 1`, or unset | Client submissions in one door session. `held` retries are not counted. | | `fake_leg_ignore_seconds` | B7 | seconds | integer `≥ 1`, or unset; gate-scoped | Time since the fake-leg refuse that entered the key on the ignore record. Unset means no expiry. | | `challenge_lifetime_seconds` | 15.1 | seconds | integer `≥ 1` | `expiry_unix_s − issue_unix_s` of a minted challenge. The website and the Gate use the same value. | | `challenge_clock_skew_seconds` | 15.1 | seconds | integer `≥ 0` | Allowance in the section 15.1 challenge window comparison. | | `result_retention_seconds` | 15.1 | seconds | integer `≥ 0` | Time a settled result row stays readable. It never shortens a consumed-nonce row, which is kept within one nonce-row store until `now_unix_s ≥ expiry_unix_s + challenge_clock_skew_seconds`; when the store could have lost rows, `nonce_store_start_unix_s` moves forward and every earlier challenge is refused (section 15.1 **Lifetime and clock skew**, **Single-use**). | **Policy file encoding.** A policy file that an admission service reads is a TOML v1.0.0 document. It has a top-level string `spec`, the version of this file it targets (`"2.3"`, or `"2.2"` for a policy written for Satoshi Hike 2.2, which this file reads by the rules of this section, so an absent key is never read as a 2.2 default), and a table `[levers]`. Each key in `[levers]` is exactly a lever name from the table above, with no alias, prefix, suffix, or unit added: `fast_accept_bound`, not `fast_accept_bound_seconds`. An integer lever is a TOML integer. `leg_rate_floor` is a TOML table with exactly the integer keys `p` and `q`, for example `{ p = 1, q = 5 }`. It is not a string and not a float. `provisional` is the string `"on"` or `"off"`. A lever whose range is positive seconds (`fast_accept_bound`, `async_catch_budget`) is a TOML integer or float with at most 3 fractional digits. The service reads it as integer milliseconds, the nearest integer to the value times 1000, and compares in those milliseconds; a value that reads as 0 milliseconds or less is out of range. An absent key means unset where the lever’s range lists unset; there is no other spelling of unset. Any other absent key makes the service refuse the whole file. A key in `[levers]` that is not a lever name, a value of the wrong TOML type, or a value outside the lever’s range makes the service refuse the whole file. It does not run with part of a file. Keys outside `[levers]`, other than `spec`, belong to the policy, and this file does not read them. These are rules of this file, not levers: `provisional_action_cap` (a `provisional` state grants no action, and no irreversible action is allowed while `provisional`); `revoke_scope`, the pair `(public_key, object_sha256)`; the round size of 6 legs (the section 14.6 draw count); the friendly-list rules in section 15.1 other than custody. `revoke_scope` binds a `revoke` to that pair. It does not end another object's grant. A later-policy re-verify that refuses, other than a B7 cap refuse, is a `revoke` and removes the friendly entry only when this object is the key's current friendly entry, by a compare-and-remove on `object_sha256`. A `not read` does not. #### Rule B0. Which rate the admission service reads **Normative.** The admission service reads `(hike_legs, hike_span)` from Rule A4.10. `leg_rate = hike_legs / hike_span`. That rate is the admission rate. The pair starts at the first interrupt `I_0` (`stages[1].beacon`) and ends at `last_interrupt` (`I_17`). It does not start at `seed_beacon`. Stage 0 checkpoints are not in `hike_legs`. The bookend pair `(N, S)` is a section 14 measurement. It is not this rate. `whole_hike_legs` and `whole_hike_span` stay analysis names for the same integers as `hike_legs` and `hike_span`. When `hike_span` is `not read`, there is no numeric rate. The state is `held` and the read is retried (`no_pair_outcome`). Bookend `S` `not read` is not this hold. Bookend `N` `not defined` is not, by itself, an admission refuse. When fewer than `min_anchored_paths` waypoints among `W_1` … `W_16` would be anchored, wherever those waypoints sit, even if every `not read` plant or path read among them came back anchored, a re-read cannot change that result. Policy `refuse`s it at once. It does not go to `held`. If a re-read could bring that count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. A short or missing network read is `not read`. The state is `held`. The service re-verifies. #### Rule B2. Leg-rate floor **Normative comparison.** `leg_rate_floor` is the lever `p/q` (section 15.4) in checkpoints per second, `hike_legs` over `hike_span`, with `hike_span` in Unix seconds. One checkpoint is one leg. Section 15.5 states the measuring method. The comparison is integer-only: a ratio `p/q` meets the floor when `hike_span` is an integer greater than 0 and `q · hike_legs ≥ p · hike_span`. The comparison result is `meets` when that holds and `refuse` when it does not (section 4, **Comparison result**). `hike_span` ≤ 0 means the rate is undefined. The outcome is `refuse`. It is not `held`. `held` is only `hike_span` `not read`. The policy supplies `p` and `q`. The comparison does not round. Rule B3 does not change `hike_span`. There is no comparison when `hike_span` is `not read`. The fast phase does not issue `provisional` on that gap. The state is `held`. A short or missing network read stays `not read` and the state is `held`. A numeric rate that falls short of the floor is `refuse`. The bookend pair is not an input to this comparison. #### Rule B3. Span sanity **Normative.** Rule B3 is not the admission rate and is not an admission input. No span adjustment is applied to bookend `S` for admission, and none is applied to `hike_span`. `delta_h` stays the Rule A4.8 measurement. #### Rule B4. Paths minimum **Normative.** The comparison is `anchored_total ≥ min_anchored_paths`, with `min_anchored_paths` the lever in section 15.4. When `anchored_total ≥ min_anchored_paths`, the comparison result is `meets` (section 4, **Comparison result**). The anchored waypoints sit anywhere among `W_1` … `W_16`. `W_17` is not one of the 16. The count is Rule A4.5. A count settled below `min_anchored_paths` by bytes already in hand is `refuse` at once. If a re-read could bring the count to `min_anchored_paths` or more, the measurement is `not read` and the state is `held`. #### Rule B6. Fixed start **Normative.** `fixed_start` is not an admission input. Section 14 may still record `fixed_start.N`, `fixed_start.S`, and `fixed_start.delta_h` when bookends are defined. The admission service does not read those three for the rate, for the anchor comparison, or for any other admission comparison. Part A still records the actual `a`, `b`, `N`, and `S`. A `fixed_start` field of `not read` or `not defined` does not, by itself, `refuse` or hold the submission. #### Rule B7. Resubmits **Normative.** `verify_cap_per_object` (section 15.4) counts verifies per `(public_key, object_sha256, policy_version)`. It is a compute limit, not a ban. A hit's refuse is final for that `submission_sha256` only. It does not change the pair's state or friendly entry. It is not the pair's thorough decision. A cap refuse (`verify_cap_per_object` or `verify_cap_per_door_session`) does not end the cycle. Service-started re-verifies and `held` retries are not in the count. The same object under a new policy version may be verified again. `verify_cap_per_door_session` (section 15.4) counts client submissions in the session, so it bounds the fast verifies and the thorough runs those submissions trigger. Held retries are Gate-internal and do not count. It is not a cap on a website session. A transport rate limit stays outside this policy. These caps are compute limits. They do not ban a key. They are not the fake-leg ignore. **Normative.** The admission service recomputes `D = 6 · thorough_rounds` distinct legs, drawn once and split into `thorough_rounds` rounds of 6, in that draw order. Each round’s 6 legs run in parallel. Section 15.5 is that rule, including the case `6 ≤ leg_total < D`. A fake leg, a section 14.6 `mismatch`, refuses that submission. That gate then ignores every later submission from that public key, for `fake_leg_ignore_seconds`, or with no expiry when that lever is unset, on the gate’s persistent per-key record. A cheap-check failure does not. The caps above are not this ignore. Cross-gate sharing of the record is out of scope. **Normative.** When the policy in force sets a value for `verify_cap_per_object` or `verify_cap_per_door_session` (section 15.4), a hit on that cap, on a valid signature and a fresh challenge, is a fast final `refuse` of that attempt. A `verify_cap_per_object` refuse is final for that `submission_sha256` only. It does not change the pair's state or friendly entry. It is not the pair's thorough decision. A cap refuse (`verify_cap_per_object` or `verify_cap_per_door_session`) does not end the cycle. A `verify_cap_per_door_session` hit stays in `received`. The submission response is `refuse`. `verify_cap_per_door_session` (section 15.4) counts client submissions in the session, so it bounds the fast verifies and the thorough runs those submissions trigger. Held retries are Gate-internal and do not count. These are compute limits. They do not ban the key. The hit does not start a thorough run and does not issue a grant. An unauthenticated rate limit is not this rule. A transport rate limit stays outside this policy. #### Rule B8. Leg count **Normative.** There is no minimum or maximum leg count as policy. There is no per-stage admission minimum and no whole-hike leg-count minimum. `leg_count ≥ 1` per stage follows from Rule A1.1 and is recorded under shape (Rule A4.1). It is the section 13.2 loop. It is not an admission threshold. A shape `mismatch` is Rule B9. It does not delete the draw rule below. The section 14.6 token `not defined`, when fewer than six checkpoints exist, is the draw’s own token. It is not a leg-count floor. The sample size `D` is a count of legs to recompute. It is not a leg-count floor. When `leg_total` < 6 the section 14.6 draw is `not defined` and the admission service refuses at once. When `leg_total` ≥ `D` the admission service draws `D` distinct global indices once, by the section 14.6 unbiased draw, discarding an index already chosen, and splits that sequence into `thorough_rounds` rounds of 6 in draw order. The 6 indices of one round are distinct. No index repeats across the `D`. When `6 ≤ leg_total < D` the same draw continues for `thorough_rounds` rounds of 6. An index is not repeated while an unused index remains. When no unused index remains, a later draw may repeat an index, and it still discards an index already chosen in the current round. The 6 indices of the current round stay distinct. Each index is one section 14.6 unbiased draw. A discarded candidate is not used and the draw continues. A `mismatch` refuses that submission. The gate does not start the next round. The index draw is one sequence. The rounds are that sequence split into sixes. They are not independent draws. ### 15.5 Thorough draw and the floor **Normative.** `D = 6 · thorough_rounds` (section 15.4). Cheap checks run on every checkpoint and every waypoint. Those checks are shape, header match, the seed, the Rule A4.3 checkpoint digest on every checkpoint, and the section 12.2 fold and waypoint rehash. The Rule A4.3 digest hashes the stored sample. It does not recompute a leg. A cheap-check failure refuses the submission and does not add the public key to the ignore record. After the cheap checks, the gate draws distinct global indices once and runs them as `thorough_rounds` rounds of 6. When `leg_total` ≥ `D` the draw is `D` distinct indices, split in draw order into rounds `[6r : 6r + 6]` for `r = 0 … thorough_rounds − 1`. When `6 ≤ leg_total < D` the draw still fills `thorough_rounds` rounds of 6, indices stay distinct while an unused index remains, and a later draw may repeat an index only after every index has been used. The current round’s 6 stay distinct. When `leg_total` < 6 the draw is `not defined` and the gate refuses. The 6 legs of a round run in parallel. A `mismatch` in a round refuses that submission. The gate does not start the next round. Legs of the current round that are already running finish only for the record; their results do not turn the refuse into an accept. **Normative.** The current group of 6 is the indices in `phase` from index `6` times the number of complete groups already in `phase`, through the end. It is empty when the length of `phase` is a multiple of 6, including at the start. A candidate already in that group is discarded. The draw has not stopped while `phase` holds fewer than `D` indices and the section 14.6 byte source still yields a candidate. The draw stops when `phase` holds `D` indices. The draw also stops when that byte source is exhausted before `D` indices. Exhaustion is `not read`. Rounds already filled stay recorded. The unfinished group is not a round. The state is `held`. It is not a `mismatch` refuse. ```text if leg_total < 6: the draw is not defined refuse else: D = 6 * thorough_rounds phase = [] # one draw, then split. Not three independent draws. while phase has fewer than D indices and the draw has not stopped: # stopped: D indices are in phase, or the byte source is exhausted before D q = one section 14.6 unbiased draw from 0 … leg_total − 1 if q is already in the current group of 6: discard q and continue if q is already in phase and some index in 0 … leg_total − 1 is not in phase: discard q and continue append q to phase rounds = the complete groups of 6 in phase, in order # a trailing group with fewer than 6 indices is not a round for round in rounds: # up to thorough_rounds start all 6 legs before recording any of them if any of the 6 is mismatch: refuse do not start the next round record the round after all 6 have a result ``` **Informative.** Detection at a fake-leg fraction `f = 0.25`, for `n` sampled legs, is `1 − (1 − f)^n`. | Rounds finished | Legs `n` | Detection | |---|---|---| | 1 | 6 | 82.2% | | 2 | 12 | 96.8% | | 3 | 18 | 99.44% | Those three figures are `1 − (1 − 0.25)^n` at `n = 6`, `n = 12`, and `n = 18`. They are not a second sample size. **Normative.** A fake leg is a section 14.6 `mismatch` on a recomputed leg. The gate refuses that submission, and that gate ignores every later submission from that public key. The ignore is per key and that gate only. Its duration is the lever `fake_leg_ignore_seconds`; unset means no expiry. Later submissions receive that same refusal, with no new draw. A cheap-check failure does not add the key. “That gate” is the admission service’s persistent per-key record. The record survives a policy-version bump and a restart of the gate. A re-verify never gives a free redraw. Cross-gate sharing of the record is out of scope. An admission service that runs one round’s 6 legs one after another does not follow this section. Rule A4.4.4 still applies to the section 14 measurement record: the record does not depend on finish order, and a verifier that is not running this thorough draw may recompute its six legs one at a time. That permission does not apply to `thorough_rounds`. The admission service starts all 6 legs of the round before any of those 6 is recorded, and it records the round only after all 6 have a result. **Floor.** `leg_rate_floor` is the lever in section 15.4. This file gives it no default. The policy in force sets it. **Measuring method.** Measure on an owned commodity host. The low end of that set is the low-end owned host (an Intel Core i7-10610U laptop, no SHA-NI). Use a conforming SDK at the cut tag of this file (section 16.2) that passes the whole pack, including every permanent negative. Use the fastest such producer build available for that host, and record with the rate whether its fold ran vectorised or scalar and whether its SHA-256 ran in hardware or software. Run a real beacon-paced hike under this file’s producer rules: nothing pauses a leg (Rule A6), and there is no wait before a path ask. The rate is the first-interrupt to last-interrupt rate, `hike_legs / hike_span`, Rule A4.10. The first interrupt is `I_0` (`stages[1].beacon`). The last interrupt is `I_17` (`last_interrupt`). The seed beacon is not the start. `hike_span` is the raw header `nTime` difference of those two beacons, `header.time` of `I_17` minus `header.time` of `I_0`. Median-time-past is not this span. The floor is calibrated on the best legal packed rate per machine. That rate is `N` parallel PCOCs with distinct keys. Distinct keys make distinct terrains under this seed. An interleave of those hikes is the packed rate. Shared-terrain packing across distinct PCOCs is impossible under this seed. It is not a legal schedule. Record the host, the CPU, the implementation, the commit, and that packed rate beside the single-hike rate. A policy that follows this method sets the floor a little below the low-end host’s rate. It does not set the floor above the rate that the released conforming SDKs reach on the low-end host. That constraint binds whoever sets or raises the floor. It is not a comparison the admission service makes. **The floor must be measured under this file.** A rate measured with a producer that paused the walk (for example, with a wait before path asks or a blocking post) is not a measurement for this file: those pauses sat inside `hike_span` and understate the rate a producer of this file reaches at the same cost per leg. This method informs the value a policy sets. It does not set one.