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:
- the Compact circuits themselves (
packages/contracts, the values the chain enforces), - the TypeScript SDK (
packages/sdk), - 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 stringx; results are 32 bytes, written as lowercase hex.‖is byte concatenation.TAG(s)is the UTF-8 encoding of the ASCII strings, right-padded with zero bytes to exactly 32 bytes. Every tag below is shorter than 32 bytes.LE32(n)is the unsigned integernas exactly 32 bytes, little-endian (least significant byte first), zero-padded.nmust be below 2^256.B32is 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 )bidis an unsigned integer below 2^64 (a CompactUint<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 elementsof the BLS12-381 scalar field, encodedLE32(s).bidderIdas 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 asstring(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 ECMAScriptJSON.stringifysince ES2019). - integer: shortest decimal, no sign for non-negative values, no exponent.
true,false,nullas 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,eligibilityKeyfor 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:
- the statement is in canonical form and names this tender and network;
- the token's
messageImprintis SHA-256 of the statement; - the CMS signature is valid, the signed
messageDigestcovers the TSTInfo, the ESS signing-certificate attribute names the signing certificate, that certificate's only extended key usage isid-kp-timeStamping(critical), and its chain reaches a pinned root with every certificate valid atgenTime; - 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.pemTest fixtures: packages/sdk/test/fixtures/tsa/ (real DigiCert and Sectigo responses over a fixed statement).
Source: docs/COMMITMENT_FORMAT.md in the Waxseal repository.