Skip to main content

Encrypting a bid

encryptBid takes a 149-byte BidPayload (already encoded and signed) and produces a TDH2 ciphertext bound to a specific CDR vault uuid. The ciphertext is what gets stored on-chain via SealedAuction.submitEncryptedBid.

This function runs in Node only. It loads @piplabs/cdr-crypto WASM at startup; the WASM glue does not work in browser or edge runtimes. If you are building a browser app, proxy this call through a Next.js API route or a server action. See Installation: WASM constraint.

Where this fits in the flow

approve WIP
allocateBidSlot ← returns uuid
signBidDigest + encodePayload
encryptBid(uuid, encodedPayload) ← THIS CALL
submitEncryptedBid

In practice, if you use placeBid you never call encryptBid directly; it is called internally. Use encryptBid directly only if you are constructing the flow step by step.

encryptBid

import {encryptBid} from '@sealedip/sdk/encrypt'
import {encodePayload, signBidDigest} from '@sealedip/sdk/payload'
import {toBytes} from 'viem'

// --- 1. Build and sign the payload ---
const nonce = ('0x' + require('crypto').randomBytes(32).toString('hex')) as `0x${string}`

const digest = signBidDigest({
auctionId: 3n,
bidder: account.address,
amount: parseEther('2.5'),
nonce,
})
const signature = await account.signMessage({message: {raw: digest}})

const payloadHex = encodePayload({
bidder: account.address,
amount: parseEther('2.5'),
nonce,
signature,
})

// --- 2. Encrypt ---
const {ciphertext} = await encryptBid({
uuid: 42, // from BidSlotAllocated event
plaintext: toBytes(payloadHex), // Uint8Array of the 149-byte payload
})

// ciphertext is a `0x${string}` ready for submitEncryptedBid

Inputs

FieldTypeDescription
uuidnumberCDR vault uuid returned by allocateBidSlot
plaintextUint8ArrayEncoded 149-byte BidPayload
globalPubKeyUint8Array?Override; normally fetched from CDR and cached

Output

interface EncryptedBid {
ciphertext: `0x${string}` // TDH2 ciphertext as hex
}

How TDH2 works here

  1. The SDK calls CDRClient.observer.getGlobalPubKey() on first use. The result is cached for the lifetime of the process (the key is stable for one DKG round).
  2. The payload bytes are encrypted via tdh2Encrypt({plaintext, globalPubKey, label}). The label is uuidToLabel(uuid).
  3. The raw cb-mpc ciphertext bytes are returned as a hex string.

Label binding is critical. The CDR validator network decrypts using the same uuidToLabel(uuid) derivation. A ciphertext labeled for uuid 42 cannot be decrypted under uuid 99, even with the same private key share set. This ties each ciphertext to the slot it was written into.

signBidDigest

import {signBidDigest, type DigestInput} from '@sealedip/sdk/payload'

function signBidDigest(d: DigestInput): `0x${string}`

interface DigestInput {
auctionId: bigint
bidder: `0x${string}`
amount: bigint
nonce: `0x${string}` // 32 bytes
}

Returns keccak256(abi.encode(auctionId, bidder, amount, nonce)). Sign the result with account.signMessage({message: {raw: digest}}) (adds the \x19Ethereum Signed Message:\n32 prefix). The contract uses the same prefix in BidPayload.recoverSigner.

This digest function is used for both bid payloads (bidder is signer) and the sealed reserve payload (seller is signer). The field named bidder in DigestInput is the signer address in both cases.

encodePayload / decodePayload

import {encodePayload, decodePayload, type BidPayloadFields} from '@sealedip/sdk/payload'

// Encode 149 bytes
const hex: `0x${string}` = encodePayload({bidder, amount, nonce, signature})

// Decode back
const fields: BidPayloadFields = decodePayload(hex)

Layout: address(20) || uint256 amount(32) || bytes32 nonce(32) || bytes signature(65) = 149 bytes.

Both functions throw if the signature is not exactly 65 bytes or if the hex is not exactly 298 nibbles.

Sealed reserve: same functions, seller signs

sealReserve (see Submitting a bid) uses the same signBidDigest + encodePayload + encryptBid pipeline, except the signer is the seller and the uuid is auction.reserveUuid (not a bid slot uuid). The on-chain BidPayload.recoverSigner is called identically at settle; the contract checks that the recovered address equals the seller.