Waxseal
Preprod testnet

Specification

Waxseal commitment format

Every value Waxseal derives, byte for byte, with conformance vectors.

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

ValueDefinitionWhere
bidder idH(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 idH(TAG("waxseal:owner-id:v1") ‖ ownerSecret)Public owner of a tender.
eligibility keyH(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:

FieldValue
format"waxseal-anchor/v1"
networknetwork id, e.g. "preprod"
tendercontract address, 64 lowercase hex
mode"clock" or "committee"
block{ "height": <number>, "hash": <64 hex> } of the block that recorded tx
txtransaction hash, 64 lowercase hex
metadataHashthe contract's metadata hash, 64 hex
closeTimethe contract's deadline as ISO 8601 UTC with milliseconds, e.g. "2026-09-27T12:20:19.000Z"
phasethe contract's stored phase: clock mode bidding, clock, settled, expired; committee mode bidding, closed, settled, expired
commitmentsevery stored commitment as { "bidderId", "commitment" } (64 hex each), sorted by bidderId
resultnull, 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).

Source: docs/COMMITMENT_FORMAT.md in the Waxseal repository.