# Waxseal commitment format

Version 1 (September 2026). This document specifies every value Waxseal derives from secret
or public inputs, byte for byte, so that anyone can recompute them without Waxseal software:
an auditor checking a bid a bidder chose to open, a court expert, or a second
implementation. It is normative for contract releases compiled with Compact 0.31.1.

Conformance vectors: `conformance/vectors.json`. Three implementations are checked against
them on every change:

1. the Compact circuits themselves (`packages/contracts`, the values the chain enforces),
2. the TypeScript SDK (`packages/sdk`),
3. an independent Python implementation using only the standard library
   (`conformance/waxseal_verify.py`), written from this document.

A deliberately wrong implementation (big-endian bid encoding) is part of the Python self
test and must fail the vectors; a suite that cannot fail proves nothing.

## 1. Notation

- `H(x)` is SHA-256 of the byte string `x`; results are 32 bytes, written as lowercase hex.
- `‖` is byte concatenation.
- `TAG(s)` is the UTF-8 encoding of the ASCII string `s`, right-padded with zero bytes to
  exactly 32 bytes. Every tag below is shorter than 32 bytes.
- `LE32(n)` is the unsigned integer `n` as exactly 32 bytes, little-endian (least
  significant byte first), zero-padded. `n` must be below 2^256.
- `B32` is an opaque 32-byte string (secrets, salts, addresses, identifiers).
- A tender address is the 32-byte contract address; its usual form is 64 lowercase hex
  characters.

This is exactly what the Compact expressions in the contracts compute:
`persistentHash<Vector<k, Bytes<32>>>([a, b, …])` is `H(a ‖ b ‖ …)`, `pad(32, "…")` is
`TAG`, and `(n as Field) as Bytes<32>` is `LE32(n)`. These equalities were established by
running the compiled circuits, not assumed.

## 2. Identifiers

| Value | Definition | Where |
|---|---|---|
| bidder id | `H(TAG("waxseal:bidder-id:v1") ‖ bidderSecret ‖ tenderAddress)` | Public key of the bidder's commitment on chain. Differs per tender, so a bidder cannot be linked across tenders. |
| owner id | `H(TAG("waxseal:owner-id:v1") ‖ ownerSecret)` | Public `owner` of a tender. |
| eligibility key | `H(TAG("waxseal:eligibility:v2") ‖ bidderSecret ‖ organiserOwnerId)` | Leaf of an organiser's allow list (clock mode). |

`bidderSecret` and `ownerSecret` are 32 random bytes held in the bidder's local vault and
never published.

## 3. Bid commitment

```
commitment = H( TAG("waxseal:commitment:v1") ‖ LE32(bid) ‖ salt ‖ bidderId )
```

- `bid` is an unsigned integer below 2^64 (a Compact `Uint<64>`); valid bids also lie in the
  tender's public range `[minBid, maxBid]`, which the commit circuit proves.
- `salt`:
  - clock mode: 32 random bytes (`B32`), used as is;
  - committee mode: a random element `s` of the BLS12-381 scalar field, encoded `LE32(s)`.
- `bidderId` as in section 2.

On chain, the contract stores `commitments[bidderId] = commitment`. An *opening* is
`(bid, salt)` for a given `bidderId`; anyone holding it can recompute the commitment and
compare. The chain never receives an opening.

## 4. Tender metadata hash

Tender metadata (title, description, category, organiser, documents, mode, committee
roster) is kept off chain; `H(canonical(metadata))` is fixed in the contract at deployment
as `metadataHash`.

`canonical(v)` is UTF-8 text produced as follows:

- object: `{` + members sorted by key + `}`, members joined by `,`, each written as
  `string(key) + ":" + canonical(value)`; members whose value is absent are omitted.
  Keys are ASCII (the schema allows no others) and sort by byte value.
- array: `[` + elements in their given order joined by `,` + `]`.
- string: `"` + characters + `"`, escaping `"` as `\"`, `\` as `\\`, U+0008 `\b`,
  U+000C `\f`, U+000A `\n`, U+000D `\r`, U+0009 `\t`, every other code point below U+0020
  and every unpaired surrogate as `\u` + four lowercase hex digits; all other characters
  unescaped (this is ECMAScript `JSON.stringify` since ES2019).
- integer: shortest decimal, no sign for non-negative values, no exponent.
- `true`, `false`, `null` as written.
- No whitespace anywhere. **No Unicode normalisation**: a title in NFC and the same title in
  NFD are different metadata with different hashes. The vectors include such a pair.

## 5. Clock mode

Public parameters: `rule ∈ {lowest, highest}`, `minBid`, `maxBid`, `step > 0`,
`closeTime` (unix seconds), `roundSeconds`, `rounds`.

```
span        = maxBid - minBid
moved(r)    = min(r · step, span)
price(r)    = minBid + moved(r)    if rule = lowest
              maxBid - moved(r)    if rule = highest
reaches(b,r)= b ≤ price(r)         if rule = lowest
              b ≥ price(r)         if rule = highest
round r is open during [closeTime + r·roundSeconds, closeTime + (r+1)·roundSeconds)
```

The *qualifying round* of a bid `b` is the smallest `r` with `reaches(b, r)`. The claim
circuit accepts a claim for round `r` only if `reaches(b, r)` holds and `reaches(b, r - 1)`
does not (for `r > 0`); so a bid can be claimed only in its qualifying round. The first
accepted claim settles the tender at `price(r)`.

## 6. What the vectors cover

`conformance/vectors.json` holds, for each case, the inputs and expected outputs:

- `bidderId`, `ownerId`, `eligibilityKey` for fixed secrets and addresses;
- clock and committee commitments, including bids 0, 1, 255, 256 and 2^64-1, all-zero and
  all-0xff salts, and committee salts 1 and r-1 (the largest field element);
- metadata hashes, including key ordering, escapes, control characters, emoji, a
  Vietnamese title in NFC and NFD, and a committee roster with a threshold;
- clock prices and qualifying rounds for both rules, including the cap at the range end.

To add an implementation: read the vectors, recompute every output, compare. Nothing needs
our permission. `python3 conformance/waxseal_verify.py selftest` shows the expected result.

## 7. Contract releases

A deployed tender is recognised as Waxseal by its verifier keys: for every circuit, the
SHA-256 of the verifier key stored on chain is compared with the published manifest
(`packages/contracts/releases.json`), which lists every release ever deployed. Key
generation is deterministic for a given circuit and compiler, so a rebuild of the same
sources yields the same fingerprints; this was checked against tenders deployed days before
the rebuild.

## 8. Anchor statement (independent time stamps)

An anchor is an RFC 3161 time stamp over a statement of a tender's public state after one
transaction. The statement is the canonical JSON (section 4 rules: keys sorted by their
UTF-8 bytes, `JSON.stringify` escaping, no whitespace, UTF-8 output) of:

| Field | Value |
|---|---|
| `format` | `"waxseal-anchor/v1"` |
| `network` | network id, e.g. `"preprod"` |
| `tender` | contract address, 64 lowercase hex |
| `mode` | `"clock"` or `"committee"` |
| `block` | `{ "height": <number>, "hash": <64 hex> }` of the block that recorded `tx` |
| `tx` | transaction hash, 64 lowercase hex |
| `metadataHash` | the contract's metadata hash, 64 hex |
| `closeTime` | the contract's deadline as ISO 8601 UTC with milliseconds, e.g. `"2026-09-27T12:20:19.000Z"` |
| `phase` | the contract's stored phase: clock mode `bidding`, `clock`, `settled`, `expired`; committee mode `bidding`, `closed`, `settled`, `expired` |
| `commitments` | every stored commitment as `{ "bidderId", "commitment" }` (64 hex each), sorted by `bidderId` |
| `result` | `null`, or `{ "winnerBidderId", "price" }` plus `"round"` in clock mode; numbers as decimal strings |

The time-stamp request carries SHA-256 of those bytes (`messageImprint`), a random nonce
and `certReq = true`, so the response includes the authority's certificate chain.

A verifier accepts an anchor when:

1. the statement is in canonical form and names this tender and network;
2. the token's `messageImprint` is SHA-256 of the statement;
3. the CMS signature is valid, the signed `messageDigest` covers the TSTInfo, the ESS
   signing-certificate attribute names the signing certificate, that certificate's only
   extended key usage is `id-kp-timeStamping` (critical), and its chain reaches a pinned
   root with every certificate valid at `genTime`;
4. the statement equals the one rebuilt from the chain after the named transaction.

Then the state it describes, including each listed commitment and the block hash, existed
no later than `genTime`. Pinned roots (`packages/sdk/src/timestamp/roots.ts`): DigiCert
Trusted Root G4, USERTrust RSA Certification Authority, and Sectigo Public Time Stamping
Root R46. The same check with OpenSSL, using `conformance/tsa-roots.pem`:

```
openssl ts -verify -data statement.json -in response.tsr -CAfile tsa-roots.pem
```

Test fixtures: `packages/sdk/test/fixtures/tsa/` (real DigiCert and Sectigo responses over
a fixed statement).
