offer — sealed-bid auction
audience: contributors
Proposed source: crates/offer/.
The sealed-bid auction organism. Searchers submit bundle bids
per slot, threshold-encrypted to the offer committee’s DKG-
produced public key. At auction close the committee runs a
threshold combine inside apply to decrypt bids, picks a
winner according to the lattice’s auction rule, and commits a
single AuctionOutcome entry for the slot. No committee
member — and no searcher other than the winner — ever sees a
losing bid’s cleartext.
Crate layout
offer::proto— wire types, serialization of sealed bids and auction outcomes. No I/O.offer::core— pure functions for sealing, combine-decrypt at close time, and winner selection.offer::node—OfferMachine,declare!items, role event loops, ticket validators.
Public facade:
offer::Offer::<B>::bid(&network, &Config) -> Bidder<B>— searcher-side writer of sealed bids.offer::Offer::<B>::outcomes(&network, &Config) -> Outcomes<B>— stream of committedAuctionOutcomes.offer::Config— const-constructible fingerprint input.
B: BundleDatum is a trait searchers implement for their
bundle type (analogous to zipnet’s ShuffleDatum). Carries
TYPE_TAG: UniqueId and MAX_BID_WIRE_SIZE: usize — bids
are ciphertexts of bounded size to preserve threshold-
encryption properties at the wire layer. See constant size
argument.
Public surface
Bid<B> stream
declare::stream! {
pub Bid<B: BundleDatum> = SealedBid<B>,
derive_id: OFFER_ROOT.derive("bid"),
producer require_ticket: SearcherJwt,
consumer require_ticket: OfferMember,
}
pub struct SealedBid<B: BundleDatum> {
pub nonce: [u8; 24], // unique per (searcher, slot)
pub slot: u64,
pub searcher: SearcherId, // from the JWT
pub ciphertext: Vec<u8>, // threshold-encrypted bundle + bid
pub _phantom: PhantomData<B>,
}
The searcher field is authenticated by the writer-side JWT
and not encrypted — this is what gets paired with
AuctionOutcome for attribution at refund time.
ciphertext decrypts to a Bundle<B> struct that carries
the bid value, the bundle contents, and the
UnsealedRef dependency:
pub struct Bundle<B: BundleDatum> {
pub bid: u128, // in chain native units
pub payload: B, // application-level bundle
pub depends_on: Option<UnsealedRef>, // reference into unseal::UnsealedPool
}
AuctionOutcome collection
declare::collection! {
pub AuctionOutcome = Vec<CommittedOutcome>,
derive_id: OFFER_ROOT.derive("outcome"),
consumer require_ticket: LATTICE_READ_TICKET,
writer require_ticket: OfferMember,
}
pub struct CommittedOutcome {
pub slot: u64,
pub winner: SearcherId,
pub bid: u128,
pub bundle: EncodedBundle, // cleartext of winning bundle only
pub evidence: CommitEvidence, // hashes of losing-bid ciphertexts
}
bundle is cleartext (the winner has no anonymity to preserve
against the builder — they want their txs included).
evidence carries hashes of every losing bid’s ciphertext so
that a post-hoc observer can check the committee considered
all bids without needing the losing plaintexts.
SearcherRegistry
declare::collection! {
pub SearcherRegistry = Map<SearcherId, SearcherBundle>,
derive_id: OFFER_ROOT.derive("searcher-registry"),
consumer require_ticket: LATTICE_READ_TICKET,
writer require_ticket: OfferMember,
}
Maintained by the committee; entries land when a searcher’s
JWT authenticates against a new SearcherId.
Internal plumbing
Derived private network keyed off OFFER_ROOT.derive("private"):
DecryptShares— per-slot threshold-decryption shares exchanged at auction close. Same shape asunsealshare gossip.
State machine
pub enum Command {
OpenAuction { slot: u64, opened_at: UnixSecs },
AcceptBid(BidAccept),
SubmitShare(DecryptShare),
CloseAuction(u64),
MarkDone(u64),
}
pub enum Query {
OpenAuctions,
BidsFor(u64),
OutcomeFor(u64),
Searcher(SearcherId),
}
Apply semantics
OpenAuction. Issued by the state-machine leader whenunseal::UnsealedPool[S]is observed. Creates an open auction window for slotSwith the configuredauction_window. Idempotent per slot.AcceptBid. Committed when a committee member observes a freshSealedBidon the publicBid<B>stream and proposes its inclusion in slotS’s auction. Validation:slotmatches an open auction.nonceis unique for this(searcher, slot)pair — duplicate nonces reject.- The ciphertext length is within
B::MAX_BID_WIRE_SIZE. searcheris admitted by the searcher JWT. First-committee-member-to-propose wins; duplicates are silent.
SubmitShare. Committee member’s threshold share for auction-close decryption of one specific bid ciphertext. Shares are indexed by(slot, bid_hash). Same validation shape asunseal::SubmitShare.CloseAuction. Idempotent per slot. When the leader observes the auction window elapsed, it issues this command. The apply handler:- Collects every
AcceptBidfor the slot. - For each bid, if
tshares have landed, combines them and decrypts the bid. - If a bid’s shares are insufficient, its ciphertext is considered non-admissible and is excluded.
- Among the admissible bids, picks the winner by the lattice’s auction rule (default: highest bid, with deterministic tie-break by blake3 of the bid ciphertext).
- Constructs
CommittedOutcomeand appends toAuctionOutcome.
- Collects every
MarkDone. GC after the slot is fully attributed by downstreamtally.
Invariants
- One outcome per slot. Enforced in
CloseAuctionby reject-if-already-closed. - Monotonic slots.
AuctionOutcome[S+1]cannot commit beforeAuctionOutcome[S]. - Losing bids stay encrypted.
CloseAuctionapply decrypts only the winning bid; losing-bid shares are discarded post-close without their cleartext ever leaving apply. - Bid admissibility is a pure function of the commit log. A replica re-applying the same commit sequence reaches the same winner.
Signature versioning
fn signature(&self) -> UniqueId {
let tag = format!(
"offer.v{WIRE_VERSION}.window={}ms.t={}.n={}.scheme={}.rule={}",
self.config.auction_window.as_millis(),
self.config.threshold.t,
self.config.threshold.n,
self.config.scheme,
self.config.auction_rule.tag(), // "highest-bid", etc.
);
UniqueId::from(tag.as_str())
}
Every knob folds in. A lattice swapping auction rule from
highest-bid to highest-profit-by-sim is a fingerprint
change.
DKG ceremony
Separate from unseal’s. Offer’s DKG produces an aggregate
public key whose secret shares are held by the offer
committee. Integrators (searchers) encrypt bids to this key
before submitting.
Rerun independently from unseal DKG — a compromised
offer-committee member does not force an unseal rerun.
ACL composition
impl Config {
pub fn bid_validator(&self) -> Validators {
// Writers: searchers authenticated by the operator's searcher JWT.
Validators::stacked().with(JwtIssuer::from(self.searcher_jwt_key))
}
pub fn member_validator(&self) -> Validators {
// Committee members: lattice-level JWT, optional TDX.
let mut v = Validators::stacked().with(JwtIssuer::from(self.operator_jwt_key));
if let Some(mrtd) = self.member_mrtd {
v = v.with(Tdx::new().require_mrtd(mrtd));
}
v
}
}
TDX on offer members is optional in v1. A lattice prioritising bid confidentiality under a stronger trust model adds TDX; most lattices run it with JWT-only admission.
Trust shape
Majority-honest committee for winner integrity; threshold cryptography for bid confidentiality against a minority of compromised members. See threat-model.md — offer.
Open questions
- Auction rule extensibility. The default rule is
highest-bid. Some lattices will want “highest-profit after
simulation against the unsealed pool”, which requires
running simulation inside offer’s committee. That is
conceptually a cross-organism call between offer and
atelier; the clean pattern is to have offer commit a
PendingOutcomewith the top-K bids and let atelier compute the actual profit. Not specified yet. - Withdrawal semantics. Searchers may want to withdraw a
bid once the slot’s
UnsealedPoolis revealed (if the revealed order flow makes the bid uneconomical). TheBundleWithdrawcommand is in the spec but withdrawal deadlines vs auction close need tightening. - Cross-chain bids. A bid targeting
ethereum.mainnetslotS1andunichain.mainnetslotS2is a single atomic desire from the searcher’s perspective but two independentAuctionOutcomecommits. Out of scope here; see cross-chain — Shape 3.
Cross-references
- The six organisms
- unseal — upstream.
- atelier — downstream.
- cryptography.md — offer
- threat-model.md — offer