ODVS v1 - draft specification
ODVS, the Open Domain Valuation Standard, is a working name for a way to reduce a domain appraisal to a small canonical record and hash it, so that anyone can recompute the hashes an appraisal carries and check them without trusting the site that produced it.
Status
Draft v1, hash version 1. The hash construction under hash version 1 is fixed: API responses already carry these hashes, so any change to it needs a new hash version. The text describing it is a draft.
- Licence: none granted yet. The choice is pending with the site owner.
- Frozen text: not yet. This document may still be clarified.
- Editions: not yet published.
- A second implementation in another language: not yet. Both current implementations are JavaScript.
- A neutral home: not yet. The specification lives on this site.
Test vectors: every vector's hash is listed below, and each vector's input can already be read in the code the verifier runs. No licence covers those inputs yet. A machine-readable vectors file will be published here once the licence is chosen.
Until all of these hold, ODVS is not yet a standard, and this page does not describe it as one.
Scope
ODVS covers how an appraisal is reduced to a canonical record and hashed. It does not cover the valuation model itself: that is the engine's job, identified by an engine id and version inside the record. The point is that two parties, or two engine versions, can compare appraisals byte for byte.
The canonical record
An appraisal keeps the fields that are a pure function of the domain and nothing else: the normalized domain, the engine id and engine version, the estimated value, the confidence score, the optional low and high bounds, and optional named numeric factor outputs (metrics). Database ids, timestamps, comparable-sale lists and anything else that depends on the environment are left out.
Every number must be finite. NaN, Infinity and -Infinity have no canonical bytes and must be refused; negative zero is folded to 0.
Domain normalization
A single pass, in this order:
- Trim with ECMAScript String.prototype.trim(), which removes the ECMAScript whitespace and line-terminator sets, including U+00A0, U+FEFF and U+2028.
- Lower-case with ECMAScript String.prototype.toLowerCase(), which is locale-independent and maps U+0130 to i followed by U+0307. Internationalized names stay in Unicode and are never converted to punycode.
- Remove a URL scheme such as https://.
- Remove everything from the earliest /, ? or #.
- Remove one leading www. and then one trailing dot.
Because it is a single pass, normalizing twice can change a result further: www.www.x.com becomes www.x.com, and a second pass would give x.com. Hash version 1 pins the single-pass result, and the vectors below check it. Repeating until nothing changes is a candidate for a later hash version.
| Input | Normalized |
|---|---|
| Example.COM | example.com |
| https://www.Acme.io/path?x=1 | acme.io |
| www.www.x.com | www.x.com |
| x.com.. | x.com. |
Serialization
Every hashed string is canonical JSON: UTF-8, no whitespace, object keys sorted recursively, so the order fields arrive in never matters.
- Keys sort by UTF-16 code unit, the ECMAScript default sort, not by code point. An emoji key therefore sorts before U+FF5A, and "10" before "9".
- Numbers are written by ECMAScript Number::toString, for example 1e+21, 1e-7 and 0.30000000000000004.
- Strings are escaped as ECMAScript JSON.stringify escapes them: quote, backslash and control characters escaped, a lone surrogate written as a
\uXXXXescape, and all other non-ASCII characters written as-is.
The three hashes
All three are lowercase hex SHA-256 over canonical JSON.
inputCommitment = SHA256({ d: domain, e: engineId, v: engineVersion })
resultDigest = SHA256(canonical result fields)
appraisalHash = SHA256({ commitment: inputCommitment, result: resultDigest, hv: 1 })The input commitment identifies the computation: the same domain on the same engine version always gives the same commitment. The result digest pins the numbers produced. The appraisal hash is the citable id of the whole appraisal.
The determinism property
For a deterministic engine, two appraisals with the same input commitment must have the same result digest. A pair that shares a commitment but differs in its digest is a determinism violation, and anyone holding both can show it.
Conformance
An implementation conforms to this draft when it reproduces every hash below and refuses each of the 3 non-finite inputs (NaN and plus or minus Infinity). The expected hashes were produced by a separate Node implementation and are checked against this site's own code by its test suite. You can run all 18 vectors against the code this site is serving in the ODVS verifier, in your own browser. The input of each vector is readable in the code the verifier runs; a licensed, machine-readable vectors file follows once the licence is chosen.
| Vector | appraisalHash |
|---|---|
| minimal | b0d5e7148407ab9f9968eb21c9a8606423a584f3c662741a317a77e01cd030a7 |
| full-with-metrics | abf569213629754a3d16af46771bddffc7a664bd82a06ae515a499b684644c94 |
| negative-zero-folds | f8b159c51daef953f292c590e552f733787b8edff33409e88d8e15fa92f8705c |
| whitespace-www-trailing-dot | a6bf40ed853d282ca0fc71714d6ecd2f89c2037d86c0d6742d312ba59e8c0280 |
| single-pass-double-www | 364c18b776bae087cc5f44749c0d447708a5f12c9811335a1dc207934d7317b2 |
| single-pass-double-trailing-dot | 4aa34d680c883a37558be560760b47e909df0af8bc6bde4371f6fe50281e32f8 |
| single-pass-space-before-path | df2075857b0f990d5d7f4aed0c89291b3e3fda28ed5bd17804e19d5af6968489 |
| idn-not-punycoded | 55b98b04596776b9551519e97b2988e204f67cb11c3fbdf19960444354ff098c |
| ecmascript-trim-set | c407304a0347246012d64009fbe8115350505e13b12dfaa146590fcaadd4351f |
| ecmascript-lowercase-dotted-capital-i | f3320b463edb8e4283a33b6c7689328a259189002a4d09aa5722aad435ed1de2 |
| metric-key-order-utf16 | 9fefd161845b4d859b9c2bdaeb9170a52bad0e6760617c3ad712a09cfc7f90a3 |
| metric-key-lone-surrogate | cdbb43315cce8ba9774fb94b4b5040c0ddd37eb99be79f4af020226998952f9c |
| ecmascript-number-format | df0db218a6457dde868285cc91275d0f4f0280dafcfcd30fbe122a2541b221ef |
| string-escaping | af4d16d797c25ce91e3dad1a94475ce5af1855cad12688c4286037ee09255425 |
| empty-metrics | 00f9b134b211d4d11bce7c40955216693bcc8002408300b9b808d62d950e69c4 |
| proto-metric-key | f65aef64f67d9480336029e02b61a1508ae1c7e09575d8c9550bcdb33517e054 |
| low-without-high | 4232316d7292798cb8d440d7913c12283a28923c2f4da339280c1b78019163bd |
| production-identity | 426263de63edefdaaf455989097c9ff64d4e93670b00967fb4d409fcf4ff72f3 |