Skip to content

Latest commit

 

History

History
214 lines (158 loc) · 10.5 KB

File metadata and controls

214 lines (158 loc) · 10.5 KB

Round Receipts

A round receipt is a portable JSON document that captures the final state of a Sub Rosa round. Receipts can be exported from any live contract and verified offline - no RPC, no secrets, no Stellar dependency.

Core v2 receipt

New partner integrations use exportReceiptV2, serializeReceiptV2, and verifyReceiptV2. The version-2 receipt binds the complete canonical payload envelope and includes schemaRef, mode, settlement assets, Drand configuration, participant cap, and partner policy.

The policy object records:

  • enforced: whether the round was created through the partner-policy path;
  • fixedEscrow: the identical Auction escrow, or zero for ReceiptOnly;
  • participation: Open or Allowlist;
  • eligibleParticipants: the public on-chain allowlist when configured.

The offline verifier recomputes each full-envelope commitment, checks the revealed amount, fixed escrow, allowlist membership, settlement flags, and deterministic winner. Legacy Core v2 rounds remain exportable with policy.enforced: false and produce a warning rather than inventing policy enforcement that did not exist on-chain.

const receipt = await client.exportReceiptV2(roundId);
const result = verifyReceiptV2(receipt);
const json = serializeReceiptV2(receipt);

The hosted pilot exposes the same verified canonical JSON download after a round reaches Settled or Voided.

Legacy v1 receipt

Schema

Field Type Description
version number Legacy receipt schema version (1)
network string Stellar network passphrase (e.g. "Test SDF Network ; September 2015")
networkFingerprint string (64 hex chars) sha256(utf8(network)) — lets the offline verifier detect a tampered network field without any caller context
contractId string Contract address (starts with C)
exportedAt string (ISO 8601) When the receipt was exported
roundId string (decimal bigint) Round number
itemRef string (hex) Item identifier (e.g. the commitment of the highest valid bid)
revealRound number Drand quicknet round used for timelock
clearingRule "HighestBid" or "LowestBid" Selection rule
commitDeadline string (decimal) Ledger sequence deadline for commit
revealDeadline string (decimal) Ledger sequence deadline for reveal
operator string (G… address) Round creator / prize sender
auditorPubkey string (hex) X25519 auditor public key
bidders string[] Ordered list of participant Stellar addresses
bids Record<string, BidReceiptEntry> Per-bidder entry, keyed by address
winner string (G… address) or null Declared winner (may be null if voided or no valid bids)
winningValue string (decimal) or null Declared winning bid value
status "Open" / "Revealing" / "Cleared" / "Settled" / "Voided" Round status

BidReceiptEntry

Field Type Description
commitment string (64 hex chars) `sha256(be16(value)
escrow string (decimal) Escrow amount
revealedValue string (decimal) or null Bid value (null if not-revealed)
nonce string (64 hex chars) or null The per-bid 32-byte nonce. Persisted on-chain at reveal time; present in live exports for revealed bids. null only for unrevealed bids.
hashValid boolean or null Whether revealedValue and nonce match commitment. null when nonce is not available (unrevealed); true/false when both are present.
valid boolean Whether the bid passed all on-chain validity checks (value ≤ escrow etc.)
settled boolean Whether escrow was settled (transferred or refunded)
evidence object or null Contains ciphertext (hex, may be null after expiry) and auditorBlob (hex, may be null after expiry)

Expired Temporary storage (ciphertext, auditorBlob) is marked honestly as null.

Serialization format

Receipts use canonical JSON — keys are deep-sorted lexicographically. BigInt values are serialized as decimal strings (not numbers) to preserve precision. Byte strings (commitments, nonces, evidence) are lowercase hex.

Offline verification

The verifyReceipt function in @sub-rosa/sdk performs stateless, offline checks:

Check What it detects Error code
Schema version Unsupported receipt format unsupported_version
Network fingerprint networkFingerprint does not match sha256(utf8(network)) — detects a tampered passphrase without caller context network_mismatch
Network metadata Missing or malformed fields missing_network, invalid_contract_id, etc.
Clearing rule Invalid or missing rule invalid_clearing_rule
Bidder list consistency Duplicates, missing bid entries, orphan entries duplicate_bidder, missing_bid_entry, orphan_bid_entry
Commitment binding For each revealed bid where nonce is present, recomputes `sha256(be16(value)
Winner selection Recomputes the winner from valid revealed bids and compares to declared winner winner_mismatch
Evidence hex format Ciphertext/auditorBlob not valid hex invalid_evidence_hex (warning)

What the verifier cannot check

  • On-chain state — the verifier does not connect to Stellar. It cannot confirm that the receipt matches what is currently stored on-chain at contractId. That is a trust choice: the exporter is responsible for honesty.
  • Drand signature correctness — the contract verifies the BLS signature. The offline verifier trusts the receipt's revealRound metadata.
  • Escrow amounts — the verifier checks internal consistency but does not independently verify balances.
  • Timing — deadlines are recorded as metadata but not cross-checked against ledger state.

Trust model

A valid receipt proves internal consistency: all revealed values bind to their commitments via sha256, and the declared winner is the correct one given the clearing rule. This is useful for:

  • Audit trails — prove a round was correctly computed after the fact
  • Dispute resolution — a participant who saved the receipt can verify the operator ran the round fairly
  • Archival — compact snapshot of a round's outcome without indexing the full chain

The receipt's trust depends on the exporter being honest about the on-chain data. Anyone can re-export and compare. For full trust, export immediately after settle and compare receipts from multiple parties.

Usage

Export from a live contract

# Requires: RPC_URL, NETWORK_PASSPHRASE, CONTRACT_ID in environment
pnpm --filter @sub-rosa/receipt-cli receipt export 1

Verify offline

pnpm --filter @sub-rosa/receipt-cli receipt verify round-1-receipt.json

Programmatic (TypeScript)

import { verifyReceipt, parseReceipt } from "@sub-rosa/sdk";
import { readFileSync } from "node:fs";

const receipt = parseReceipt(readFileSync("round-1-receipt.json", "utf-8"));
const result = verifyReceipt(receipt);
console.log(result.valid ? "✓ Receipt valid" : "✗ Receipt invalid");
console.table(result.issues);

Redaction for public demos

The SDK ships a pure redactReceipt helper that produces a public-safe copy of any round receipt. The original receipt is never mutated.

Usage

import { redactReceipt, serializeReceipt, parseReceipt } from "@sub-rosa/sdk";
import { readFileSync, writeFileSync } from "node:fs";

const receipt = parseReceipt(readFileSync("round-1-receipt.json", "utf-8"));
const redacted = redactReceipt(receipt);
writeFileSync("round-1-receipt.redacted.json", serializeReceipt(redacted), "utf-8");

CLI

pnpm --filter @sub-rosa/receipt-cli receipt redact round-1-receipt.json

This writes round-1-receipt.redacted.json next to the source file.

What gets redacted

By default the following fields are replaced with <redacted> or indexed placeholders:

  • operator, winner, contractId, roundId, itemRef, revealRound
  • bidders array (replaced with <redacted:0>, <redacted:1>, …)
  • bids object keys (replaced with the same <redacted:N> placeholders)
  • evidence.ciphertext and evidence.auditorBlob
  • Any other field whose name matches a sensitive key such as memo, txHash, accountId, etc.

Non-sensitive proof metadata is preserved so reviewers can still inspect the cryptographic binding inside each bid: commitment, escrow, revealedValue, nonce, hashValid, valid, settled, and round-level metadata such as network, networkFingerprint, version, clearingRule, and status.

Keep-list

Use keep to preserve specific fields for review or partial verification:

const redacted = redactReceipt(receipt, {
  keep: [
    "bidders",
    "winner",
    "bids.GA4GN2X7YQKQJF5Y5X3X5X3X5X3X5X3X5X3X5X3X5X3X5X3X5X3X5X3",
  ],
});
  • keep: ["bidders"] preserves the entire bidder list.
  • keep: ["bids"] preserves the entire bids object with original keys.
  • keep: ["bids.GA4..."] preserves a single bid entry and its nested evidence.
  • keep: ["bids.GA4... .commitment"] preserves just the commitment field.

Determinism

redactReceipt is fully deterministic: calling it repeatedly on the same receipt yields identical serialised output. This makes it safe to use in automated demo pipelines and snapshot tests.

Fixtures

Test fixtures live in services/receipt-cli/src/fixtures/:

Fixture Expectation
golden.json 3 bidders, HighestBid, bidder 2 wins with 250 — passes all checks
testnet-proof.json 2 bidders, round 42 — passes all checks (represents a real testnet export)
tampered-winner.json Declared winner differs from computed winner — winner_mismatch
tampered-values.json Revealed values swapped so commitments don't bind — commitment_mismatch
tampered-commitment.json One commitment hash replaced with garbage — commitment_mismatch
tampered-network.json Passphrase changed to mainnet but networkFingerprint kept as testnet — always fails with network_mismatch
tampered-order.json Tied bids, bidders reordered so winner changes — winner_mismatch
tampered-evidence.json Invalid hex in evidence ciphertext — invalid_evidence_hex

Run fixture tests:

pnpm --filter @sub-rosa/receipt-cli test