Resolution Attestation Profile v1 (RAP-1)

A machine-checkable way for an off-chain resolver to show a venue that it followed its own declared resolution procedure.

Profile IDRAP-1
Taxonomy version1.1.0
StatusPublished specification. No venue has adopted it. No regulator has reviewed it.
Reference implementationsecurity.process-attestation — BlindOracle SKU, $0.25/call
EndpointPOST https://api.craigmbrown.com/v1/services/security.process-attestation
Identity bindingERC-8004 passport (optional — see §8)
Published2026-09-05 (v1.1.0, same day)
AuthorCraig M. Brown / BlindOracle
LicenseThis document may be freely implemented.
What this proves, and what it does not. RAP-1 proves that a submitted evidence log is internally consistent with a declared procedure. It does not prove the log is a true record of what happened — a fabricated but internally consistent log scores identically to a genuine one. Under the symmetric hmac-sha256 scheme it also does not prove who produced the evidence; use ed25519 (§7.3) when attribution matters. Every deliverable carries these limits as machine-readable fields (evidence_basis, limitation, signature_binding), not merely as prose.

1. The problem

An event contract settles against an outcome. Somebody has to determine that outcome. At a CFTC-regulated venue that determination is a human process against a pre-specified authoritative source — Kalshi, for example, resolves against official government statistics, league results and regulatory announcements, with an Outcome Review Committee for ambiguous cases. On decentralized venues it is an optimistic oracle with a dispute window.

Either way, the venue carries the settlement-integrity risk and the resolver carries none of it. The resolver says "I followed the procedure." The venue has no artifact that would let it, or its auditor, or its regulator, check that claim later.

This became a live regulatory question on 2026-06-10, when the CFTC issued a Notice of Proposed Rulemaking amending Rule 40.11. Among the grounds on which an event contract may be deemed contrary to the public interest, the proposal names settlement integrity deficits — specifically a lack of clear and objective resolution criteria. Comments closed 2026-07-27.

At the same time the supply of regulated venues expanded: Payward closed its $550M acquisition of Bitnomial on 2026-05-04, giving Kraken a Designated Contract Market, a Derivatives Clearing Organization and a Futures Commission Merchant in one transaction, with perpetual futures brought onshore through a 2026-05-29 filing.

"Clear and objective resolution criteria" is a property of a declared procedure. Whether the procedure was followed on a given resolution is a separate, checkable question. RAP-1 is a format for asking the second question mechanically.

What this is not: it is not a resolution oracle, not a data source, not a dispute mechanism, and not a substitute for a venue's own review process. It sits beside them and produces one artifact: a re-computable verdict on whether a submitted evidence log is internally consistent with a declared procedure.


2. Model

Two documents in, one verdict out.

  declared_process   ──┐
  (what you said       ├──►  A1 … A7  ──►  verdict ∈ {conformant,
   you would do)       │                              indeterminate,
  run_evidence       ──┘                              non_conformant}
  (what you recorded
   yourself doing)

The verifier never reads the resolver's systems and never reads its own operator's ledgers. The data direction is resolver → verifier. This is deliberate: a verifier that attested to its own runs would have no third-party value.

Verification is recomputation, not judgment. There is no language model anywhere in the path. The predicate grammar (§5) is closed and non-Turing-complete, evaluated under a node budget, with no dynamic eval of any kind. Two parties running the same inputs get the same verdict, and either can reproduce the other's.

2.1 Ternary verdicts

Every check returns one of four values, and the distinction between the last two is the load-bearing design decision in this profile:

Check verdictMeaning
passthe evidence affirmatively supports the claim
failthe evidence contradicts the claim
unverifiablethe evidence does not speak to the claim
not_applicablethe declaration did not make this kind of claim at all

unverifiable is never scored as fail. A resolver that omits timestamps has not been caught doing anything wrong; it has produced a log that cannot answer a timeline question. Collapsing those two states is how an attestation format becomes a false-accusation machine, and an attestation may end up in front of the resolver's own auditor or counsel.

2.2 Aggregation

any check fail          → non_conformant
else any unverifiable   → indeterminate
else                    → conformant

not_applicable checks are excluded from aggregation. So is a check the submitter opted out of entirely — currently only A7 with zero signed records. Never attempting a stronger guarantee is a normal minimal submission and must not drag an otherwise-clean result down; partially attempting one is ambiguous and is scored indeterminate. Zero evidence records never scores conformant by default.

indeterminate is the expected verdict for a good-faith first submission. conformant requires the resolver to have instrumented for it in advance.


3. Input: declared_process

{
  "ordered": true,                       // optional; enables A2
  "window": {                            // optional; enables the A4 window test
    "start": "2026-09-05T00:00:00Z",
    "end":   "2026-09-05T23:59:59Z"
  },
  "forbidden": ["manual_override"],      // optional; enables A5
  "required": [                          // ≤ 200 entries, objects only
    {
      "id": "fetch_authoritative_source",
      "predicate": { /* §5 */ }          // optional; enables A3 for this step
    },
    { "id": "compare_to_contract_terms" },
    { "id": "publish_resolution" }
  ]
}

required steps must be objects with an id. The natural shorthand ["fetch", "publish"] is rejected at the boundary rather than silently promoted to {"id": "fetch"} — a coerced step carries no predicate, and would then score as a pass the submitter never actually declared.

4. Input: run_evidence

An ordered list of ≤ 4,000 record objects. Every field is optional; each one you supply unlocks a check.

{
  "step_id": "fetch_authoritative_source",   // A1, A2, A3, A5
  "ts": "2026-09-05T14:02:11Z",              // A4  (ISO-8601)
  "fired": true,                             // A3  (or "outcome"; must be boolean)
  "predicate_inputs": {                      // A3
    "source_status": 200,
    "source_id": "bls.gov/cpi"
  },
  "prev_sha256": "…",                        // A6
  "signature": "…",                          // A7
  "sig_scheme": "hmac-sha256",               // A7
  "pubkey": "…"                              // A7
}

Where a step appears more than once, checks use its first occurrence.


5. Predicate grammar

A closed expression language over predicate_inputs. Four node types; anything else evaluates to unknown.

{ "cmp": { "field": "source_status", "op": "eq", "value": 200 } }
{ "all": [ <node>, … ] }
{ "any": [ <node>, … ] }
{ "not": <node> }

Operators: eq ne lt lte gt gte in contains.

Evaluation is Kleene three-valued: all is false if any child is false, unknown if any child is unknown, else true; any is true if any child is true, unknown if any child is unknown, else false. A missing field, an unknown operator, an empty all/any list, or a TypeError on comparison all yield unknown — never a silent false.

Bounds: 2,000 nodes, depth 25. Exceeding either yields unknown.


6. The seven checks

IDNamefail whenunverifiable whennot_applicable when
A1required steps presenta declared step has no evidence recordno required steps declared
A2declared order honoredfirst-seen order of ≥2 declared steps contradicts declared orderordered not set, or <2 orderable steps present
A3predicate recomputationa recomputed predicate contradicts the record's own fired/outcomeinputs missing, claim non-boolean, or predicate → unknownno step declares a predicate
A4timeline consistencytimestamps non-monotonic, or outside a declared windowsome timestamps unparseablefewer than 2 timestamped records
A5forbidden steps absenta declared-forbidden step_id appearsno forbidden steps declared
A6evidence chain linkagea prev_sha256 does not match the recomputed hashno record asserts prev_sha256, or only some doexactly one record (nothing to chain)
A7signature verificationa signature fails to verifyan unsupported sig_scheme, an unavailable verifier, or no signed records (opt-out)

A7 carries a binding field on pass, mirrored at the top level as signature_binding: attributable (ed25519), tamper_evident_only (hmac-sha256 or a mix), or unsigned. Two submissions can both score A7 pass and mean very different things, so never read the verdict without the binding.

A3 is the substantive one. A1/A2/A5 check that the shape of the run matches the shape of the declaration. A3 recomputes the resolver's decision logic from the inputs it recorded and compares the result to what it claims it decided. A predicate_mismatch means the resolver's own recorded inputs do not produce the outcome it published — the closest thing in this profile to catching a wrong resolution.

A3 is also the check most likely to be unverifiable in practice, because it requires the resolver to have recorded predicate_inputs at the time, in the format the declared predicate references.


7. The recipe

This section is the artifact that was missing until this document. Without it, a third party could verify an attestation but could not independently produce evidence that would earn a conformant verdict, which made conformant a state only the implementer could reach.

7.1 Canonical JSON

Deterministic serialization, used for both hashing and signing:

UTF-16 surrogate pair, e.g. 😀😀)

⚠️ The ASCII-escaping rule is load-bearing and easy to get wrong. JavaScript's JSON.stringify does not escape non-ASCII; Python's json.dumps does, by default. A producer that emits "café" where the verifier hashes "café" computes a different digest, and A6 and A7 both fail on any record containing a single accented character or emoji. Version 1.0.0 of this document shipped a TypeScript reference with exactly that defect; it is corrected below.

Reference (TypeScript):

const NON_ASCII = new RegExp("[\\u0080-\\uFFFF]", "g");
const asciiEscape = (s: string): string =>
  s.replace(NON_ASCII, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));

const canonicalJson = (value: unknown): string => {
  if (value === null || typeof value !== "object") return asciiEscape(JSON.stringify(value));
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
  const obj = value as Record<string, unknown>;
  const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
  return `{${keys.map((k) => `${asciiEscape(JSON.stringify(k))}:${canonicalJson(obj[k])}`).join(",")}}`;
};

Python equivalent: json.dumps(obj, sort_keys=True, separators=(",", ":")) — the default ensure_ascii=True is the behaviour above; do not pass ensure_ascii=False.

Both implementations above were run against the same inputs — including accented characters and an astral emoji — and produce byte-identical output.

7.2 Chain linkage (A6)

For each record after the first:

prev_sha256 = sha256( canonical( record[i-1]  minus  {prev_sha256, signature, sig_scheme, pubkey} ) )

The four excluded keys are removed before canonicalization. Excluding them is what makes the chain computable in a single forward pass: a record's hash cannot depend on its own signature.

7.3 Signature (A7) — two schemes, two different guarantees

Both sign the same payload:

payload = canonical( record  minus  {signature, sig_scheme, pubkey} )

**ed25519 — attributable. Use this when a counterparty must be able to say who produced the evidence.**

signature  = ed25519_sign( private_key, payload )      -> 64 bytes, hex-encoded (128 chars)
pubkey     = raw ed25519 public key                    -> 32 bytes, hex-encoded (64 chars)
sig_scheme = "ed25519"

The public key cannot sign. A7 pass therefore binds the evidence to the holder of the private key, and the verifier reports binding: "attributable".

hmac-sha256 — tamper-evident only.

signature  = HMAC-SHA256( key = pubkey_string_as_utf8_bytes, message = payload )
sig_scheme = "hmac-sha256"

Note the key is the pubkey string itself, UTF-8 encoded — not hex-decoded bytes. Implementers must match this byte-for-byte or A7 will fail.

Because the verification key is the signing key and travels inside the record, anyone who can check the signature can also forge it. A7 pass under this scheme means the bundle has not been altered since assembly; it attributes nothing, and the verifier reports binding: "tamper_evident_only". A submission mixing both schemes is reported at the weaker level.

Where records are produced inside a trusted execution environment, derive the key per run so the underlying secret never leaves the enclave:

pubkey = sha256_hex( `${signing_key}:${run_id}` )       # hmac-sha256 only

An unknown or unsupported sig_scheme — and an ed25519 verifier that is unavailable on the machine running the check — yields unverifiable, never fail. An absent verifier is a fact about that machine, not about the evidence.

7.4 Complete producer

import { sha256 } from "@noble/hashes/sha256.js";
import { hmac }   from "@noble/hashes/hmac.js";
import { ed25519 } from "@noble/curves/ed25519.js";
import { bytesToHex } from "@noble/hashes/utils.js";

const CHAIN_EXCLUDE = ["prev_sha256", "signature", "sig_scheme", "pubkey"];
const SIG_EXCLUDE   = ["signature", "sig_scheme", "pubkey"];

const canonicalBytes = (rec: Record<string, unknown>, exclude: string[]): Uint8Array => {
  const trimmed = Object.fromEntries(
    Object.entries(rec).filter(([k]) => !exclude.includes(k)));
  return new TextEncoder().encode(canonicalJson(trimmed));   // §7.1 — ASCII-escaped
};

/** scheme "ed25519": pass a 32-byte private key. scheme "hmac-sha256": pass a secret
 *  string, and the per-run key is derived from it. */
export function sealEvidence(records: Record<string, unknown>[],
                             opts: { scheme: "ed25519"; privateKey: Uint8Array }
                                 | { scheme: "hmac-sha256"; signingKey: string; runId: string }) {
  const pubkey = opts.scheme === "ed25519"
    ? bytesToHex(ed25519.getPublicKey(opts.privateKey))
    : bytesToHex(sha256(new TextEncoder().encode(`${opts.signingKey}:${opts.runId}`)));

  const out = records.map((r) => ({ ...r } as Record<string, unknown>));
  for (let i = 0; i < out.length; i++) {
    if (i > 0) {
      out[i].prev_sha256 = bytesToHex(sha256(canonicalBytes(out[i - 1], CHAIN_EXCLUDE)));
    }
    const payload = canonicalBytes(out[i], SIG_EXCLUDE);
    out[i].signature = opts.scheme === "ed25519"
      ? bytesToHex(ed25519.sign(payload, opts.privateKey))
      : bytesToHex(hmac(sha256, new TextEncoder().encode(pubkey), payload));
    out[i].sig_scheme = opts.scheme;
    out[i].pubkey     = pubkey;
  }
  return out;
}

7.5 Conformance vector

Self-test your implementation against this. With signing_key = "vault-secret-example" and run_id = "run-001":

pubkey = b803b4ca95c6ea3da426ca0c4be8bad67844c9c9ac1e499192f9b1e73626fee3

Sealing these three records —

[{"step_id":"fetch_authoritative_source","ts":"2026-09-05T14:02:11Z",
  "fired":true,"predicate_inputs":{"source_status":200}},
 {"step_id":"compare_to_contract_terms","ts":"2026-09-05T14:02:13Z"},
 {"step_id":"publish_resolution","ts":"2026-09-05T14:02:15Z"}]

— must produce exactly:

#prev_sha256signature
0(none)0c91ffa4b78b01f7e4d565d76cc26db64ac9457eda3a46454db6622cf5d9678e
18779deaceec478a7691921f59731ea1f8803857890840c03aef7c68e47e556c0c1ce1ce7a1f985831cb5bf6bbe4aaf64dffadcedb967d95180c4dd9377c0c6ca
2bbdad7533d68dda9767596094d3b44990d016468cfdcb12d1738bb0d95c70c74a4fbce08d247c25d6735b7e1b969b770fc65339a5539e419fc02e486ebcab7ca

Submitted against the declared_process in §10 this yields conformant — A1/A2/A3/A4/A6/A7 pass, A5 not_applicable, signature_binding tamper_evident_only, attestation_id 2760826254c56fb5cf379ddbc1737d37.

ed25519 vector. With the RFC 8032 seed 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bcc7cae0d8d0a over the same three records:

pubkey = 400237577b4006fc627b3fa445cecfae5700b711c85b52c8a110843d7004b0a7
#signature
0759453d78188a0a890e0560589de9dcdb656de0f81c2da41b5ae300c3bf011551dd567466d956f14467974efc7c2daec05bc7cea7292c652b4ca181fed522306
1542cabdadb3c84591188103f352e7c809188ebbb43430e54de017aa9ba10260e4b41077d0e62519838bf659da247a6a768d97b8684cf5d0ac16c040f192a5f0f
2c1eafd14dcca9c8b1d01e0fea28008ef578bfa3f99c82752201f30522c1c38bf4d947aafcad346bcfb2f455ec933c2004689865c8c994ff871fb62b5020c7501

prev_sha256 values are identical to the table above — the chain hash excludes all signature fields, so it does not depend on the scheme. Verdict: conformant, signature_binding attributable, attestation_id 96112955d196a0b82337a20ae030b059.

Negative controls. All four must hold; an implementation that passes any of them has a broken canonicalization and must not be used.

MutationRequired result
edit any field without re-sealingnon_conformant, A6 fail, A7 fail
replace a signature with zerosnon_conformant, A7 fail
sign with a different ed25519 keynon_conformant, A7 fail
malformed key/signature hex or wrong lengthindeterminate, A7 unverifiablenever fail

These vectors were produced by an implementation written from §7.1–§7.4 alone, in a different language from the reference, and checked against the live verifier; the corrected §7.1 canonicalization was separately cross-checked between TypeScript and Python on accented and astral input. That round-trip is what makes §7 publishable rather than merely descriptive.


8. ERC-8004 binding

RAP-1 works with no identity layer at all. Binding it to an ERC-8004 passport answers a different question — which resolver produced this attestation, and what is its history — and is what makes a series of attestations a track record rather than a pile of unrelated documents.

QuestionAnswered by
Did the resolver follow its declared procedure on this resolution?RAP-1 verdict + attestation_id
Which resolver?ERC-8004 passport, resolved from the caller's key
Has it done this before, and how did those go?the passport's accumulated attestation history
Can an outside party check any of it without asking us?the recipe in §7 + the published check definitions in §6

attestation_id is sha256(canonical({checks, verdict}))[:32] — content-addressed, so the same inputs always produce the same id, and an id that does not reproduce is evidence the artifact was altered.

The passport is the addressable, portable half; the attestation is the per-event half. Neither is useful to a venue without the other: an identity with no record of conduct is a name, and a record of conduct with no identity cannot be attributed.

8.1 Why attestations are not written to a validation registry

They are off-chain JSON artifacts. Content-addressing (above) makes one tamper-evident once you hold it; it does not make it discoverable or third-party-timestamped.

The obvious home for that is ERC-8004's validation registry — and it does not exist yet. Checked 2026-09-05 against the registry contracts curated by the 8004 team: they publish IdentityRegistry and ReputationRegistry addresses across 40+ chains, and no ValidationRegistry address on any of them, noting that this portion of the spec is "still under active update and discussion with the TEE community" and will be revised in a follow-up spec update later this year.

On Base mainnet (chain 8453) the two live registries are:

RegistryAddressStatus
Identity0x8004A169FB4a3325136EB29fA0ceB6D2e539a432live (BlindOracle is agent 60979)
Reputation0x8004BAa17C55a88189AE136b182e5fdA19dE9b63live
Validationnot deployed, spec in flux

So this is a blocked dependency, not an omission. Two workable interim paths exist and neither is implemented here:

1. Reputation registry. giveFeedback(...) takes a trailing bytes32, which could carry an attestation_id. This overloads a reputation primitive to carry a validation artifact — expedient, and arguably a misuse that would need unwinding once the real registry lands. 2. Independent Merkle anchoring. Batch attestation ids into a root and anchor that to Base, which is what BlindOracle's existing proof-anchoring rail already does for other proof kinds. Chain-agnostic and survives the spec settling, at the cost of being our own scheme rather than a standard one.

Adopt neither on the strength of this document. Until the validation registry ships, an attestation's discoverability is whatever the parties agree to out of band, and this profile says so rather than implying an on-chain guarantee it does not provide.

9. Conformance levels

Levels describe what the evidence supports, so a resolver can state its posture without over-claiming.

LevelRequiresMeaning
RAP-1-BASICverdict != non_conformant, A1 passsteps declared and observed
RAP-1-LOGICBASIC + A3 passrecorded inputs reproduce the published outcome
RAP-1-CHAINEDLOGIC + A6 passevidence is hash-linked and order-fixed
RAP-1-SEALEDCHAINED + A7 pass under hmac-sha256bundle is tamper-evident, not attributable — the verification key travels in the record and can also sign
RAP-1-ATTRIBUTEDCHAINED + A7 pass under ed25519 (signature_binding: attributable)evidence is additionally bound to the holder of a private key

There is deliberately no level that asserts the resolution was correct. No combination of these checks can support that claim: the evidence is submitter-supplied (a fabricated but consistent log scores identically), and the predicate grammar cannot express judgment-based criteria at all.


10. Calling it

curl -s -X POST https://api.craigmbrown.com/v1/services/security.process-attestation \
  -H 'Content-Type: application/json' \
  -d '{
    "declared_process": {
      "ordered": true,
      "required": [
        {"id": "fetch_authoritative_source",
         "predicate": {"cmp": {"field": "source_status", "op": "eq", "value": 200}}},
        {"id": "compare_to_contract_terms"},
        {"id": "publish_resolution"}
      ]
    },
    "run_evidence": [
      {"step_id": "fetch_authoritative_source", "ts": "2026-09-05T14:02:11Z",
       "fired": true, "predicate_inputs": {"source_status": 200}},
      {"step_id": "compare_to_contract_terms",  "ts": "2026-09-05T14:02:13Z"},
      {"step_id": "publish_resolution",         "ts": "2026-09-05T14:02:15Z"}
    ]
  }'

Priced at $0.25/call over x402/USDC on Base. Malformed input returns a structured error envelope distinguishing validation (the submission) from internal (a defect in the verifier) — a verifier bug must never be reported as the submitter's fault.

The handler never raises: an unexpected fault returns an internal error rather than an HTTP 500 the caller paid for.


11. Prior evidence

A production Chainlink CRE workflow produced hash-chained, HMAC-signed evidence inside a trusted execution environment and submitted it to this verifier on 2026-09-03. Attestation 24a3f06d… returned conformant — A1 pass, A2 pass, A4 pass, A6 pass, A7 pass; 0 fail, 0 unverifiable; A3 and A5 not_applicable (no predicates or forbidden steps declared). The per-run signing key was derived from a vault secret inside the enclave, so the secret never left it.

That run is the existence proof that the format is producible under real constraints. It is not evidence that the format proves what a venue needs, for the reasons in the evidence remains submitter-supplied, and that run used the symmetric scheme, so it is tamper-evident rather than attributable.


12. Changelog

VersionDateChange
1.0.02026-09-05First publication. Recipe published.
1.1.02026-09-05ed25519 scheme added — A7 can now attribute evidence to a keyholder, reported as signature_binding. Canonicalization defect in the v1.0.0 TypeScript reference corrected (§7.1): it did not ASCII-escape non-ASCII characters, so any record containing an accented character or emoji failed A6/A7 against the verifier. Conformance vectors updated.

13. References