import test from 'node:test'; import assert from 'node:assert/strict'; import { generateKeyPairSync, sign, randomUUID } from 'node:crypto'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { NETWORK, nftTransferMessage, nftTradeMessage, type NftTransfer, type NftTrade } from '../shared/testnet.ts'; import { SALE_CANDIDATE, saleTermsMessage, saleCompletionMessage, saleCancellationMessage, saleTermsSchema, type SaleTerms, type SaleTransaction } from '../shared/sale-candidate.ts'; import { applySaleBlock, initialSaleState, saleContext, saleDecision, saleId, saleStateRoot, termsForCustodyOrder, type SaleState, type CandidateTransaction } from '../server/testnet/sale-candidate.ts'; import { address, transactionSchema, CHAIN_ID } from '../server/testnet/consensus.ts'; import { CustodyMarket } from '../server/market/custody.ts'; import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts'; import { CUSTODY_POLICY, custodyMessage, type CustodyCommand, type CustodyAction, type CustodyOrder, type CustodyListing } from '../shared/custody.ts'; function identity() { const k = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const publicKey = k.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); const owner = address(publicKey); return { owner, authorization: (message: string) => ({ publicKey, signature: sign('sha256', Buffer.from(message), { key: k.privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64') }) }; } const seller = identity(), buyer = identity(), host = identity(), attacker = identity(); const card = 'a'.repeat(64), second = 'b'.repeat(64); // These heights are deliberately tiny test parameters, NOT production confirmation-policy approval. const context = saleContext({ coinbaseMaturity: 2, lockDepth: 2, outcomeDepth: 3, maxLockBlocks: 30, maxTransactions: 64 }); function fixture(overrides: Partial = {}) { const terms: SaleTerms = { protocol: SALE_CANDIDATE, network: NETWORK, chainId: context.chainId, policyDigest: context.policyDigest, custodianId: randomUUID(), orderId: randomUUID(), cardHash: card, sequence: 0, seller: seller.owner, buyer: buyer.owner, host: host.owner, priceAtomicXmr: '100000000000', feeBps: 250, lockUntilHeight: 12, expiresHeight: 20, ...overrides }; const state = initialSaleState({ [card]: { owner: seller.owner, sequence: 0, mintedHeight: 1, pageId: 1 }, [second]: { owner: buyer.owner, sequence: 0, mintedHeight: 1, pageId: 2 } }, 9); const lock: SaleTransaction = { type: 'sale-lock', terms, sellerAuthorization: seller.authorization(saleTermsMessage(terms)), buyerAuthorization: buyer.authorization(saleTermsMessage(terms)) }; const ref: { protocol: typeof SALE_CANDIDATE; chainId: string; saleId: string } = { protocol: SALE_CANDIDATE, chainId: context.chainId, saleId: saleId(terms) }; const completion = { ...ref, reservationDigest: 'f'.repeat(64) }; const complete: SaleTransaction = { type: 'sale-complete', ...completion, hostAuthorization: host.authorization(saleCompletionMessage(completion)) }; const cancel: SaleTransaction = { type: 'sale-cancel', ...ref, hostAuthorization: host.authorization(saleCancellationMessage(ref)), buyerAuthorization: buyer.authorization(saleCancellationMessage(ref)) }; const expire: SaleTransaction = { type: 'sale-expire', ...ref }; return { terms, state, lock, complete, cancel, expire }; } const apply = (s: SaleState, txs: unknown[] = []) => applySaleBlock(s, txs, context); function at(s: SaleState, height: number) { while (s.height < height) s = apply(s); return s; } function gift(sequence: number): CandidateTransaction { const terms = { network: NETWORK, chainId: context.chainId, cardHash: card, from: seller.owner, to: attacker.owner, sequence }; const signature = seller.authorization(nftTransferMessage(terms)); return { type: 'native-transfer', transfer: { ...terms, ...signature } }; } function bundle(sequence: number): CandidateTransaction { const terms = { type: 'trade' as const, network: NETWORK, chainId: context.chainId, proposer: seller.owner, counterparty: buyer.owner, offered: [{ cardHash: card, sequence }], requested: [{ cardHash: second, sequence: 0 }] }; const a = seller.authorization(nftTradeMessage(terms)), b = buyer.authorization(nftTradeMessage(terms)); return { type: 'native-trade', trade: { ...terms, proposerPublicKey: a.publicKey, proposerSignature: a.signature, counterpartyPublicKey: b.publicKey, counterpartySignature: b.signature } }; } test('sale candidate is domain separated and all candidate transactions remain rejected by active consensus', () => { const f = fixture(); assert.notEqual(context.chainId, CHAIN_ID); for (const tx of [f.lock, f.complete, f.cancel, f.expire]) assert.equal(transactionSchema.safeParse(tx).success, false); const transfer = gift(0) as { type: 'native-transfer'; transfer: NftTransfer }; const trade = bundle(0) as { type: 'native-trade'; trade: NftTrade }; assert.equal(transactionSchema.safeParse(transfer.transfer).success, false); assert.equal(transactionSchema.safeParse(trade.trade).success, false); assert.throws(() => applySaleBlock(f.state, [f.lock], { ...context, chainId: CHAIN_ID }), /context/); assert.equal(saleTermsSchema.safeParse({ ...f.terms, unexpected: true }).success, false); assert.equal(saleTermsSchema.safeParse({ ...f.terms, priceAtomicXmr: '1.0' }).success, false); }); test('lock requires both parties, commits all economic/context/window fields and never changes owner', () => { const f = fixture(), locked = apply(f.state, [f.lock]); assert.equal(locked.nfts[card].owner, seller.owner); assert.equal(locked.nfts[card].sequence, 1); assert.equal(locked.locks[card], saleId(f.terms)); assert.equal(f.state.nfts[card].sequence, 0); assert.throws(() => apply(f.state, [{ ...f.lock, buyerAuthorization: attacker.authorization(saleTermsMessage(f.terms)) }]), /both trader/); for (const patch of [{ orderId: randomUUID() }, { custodianId: randomUUID() }, { priceAtomicXmr: '1' }, { feeBps: 500 }, { lockUntilHeight: 13 }, { expiresHeight: 21 }, { buyer: attacker.owner }, { host: attacker.owner }, { sequence: 1 }, { policyDigest: 'c'.repeat(64) }]) { assert.throws(() => apply(f.state, [{ ...f.lock, terms: { ...f.terms, ...patch } }])); } const changedPolicy = saleContext({ ...context.policy, outcomeDepth: 4 }); assert.notEqual(changedPolicy.chainId, context.chainId); assert.notEqual(saleId({ ...f.terms, custodianId: randomUUID() }), saleId(f.terms)); }); test('host cannot redirect delivery or alter signed reservation evidence, and completion waits for lock depth', () => { const f = fixture(), locked = apply(f.state, [f.lock]); assert.throws(() => apply(locked, [f.complete]), /mature lock window/); const mature = at(locked, 11); assert.throws(() => apply(mature, [{ ...f.complete, reservationDigest: 'e'.repeat(64) }]), /host authorization/); assert.throws(() => apply(mature, [{ ...f.complete, hostAuthorization: attacker.authorization(saleCompletionMessage(f.complete as Extract)) }]), /host authorization/); assert.throws(() => apply(mature, [{ ...f.complete, to: attacker.owner }])); const done = apply(mature, [f.complete]); assert.equal(done.nfts[card].owner, buyer.owner); assert.equal(done.nfts[card].sequence, 2); assert.equal(done.sales[saleId(f.terms)].reservationDigest, 'f'.repeat(64)); assert.equal(saleDecision(done, f.terms, context).action, 'hold'); assert.equal(saleDecision(at(done, 14), f.terms, context).action, 'settle'); assert.equal(saleDecision(at(done, 14), f.terms, context, 'settled').action, 'none'); }); test('candidate host recipient extension is signed and omission preserves historical terms bytes', () => { const legacy = fixture(), t = legacy.terms; assert.equal(saleTermsMessage(t), JSON.stringify([SALE_CANDIDATE, 'terms', t.network, t.chainId, t.policyDigest, t.custodianId, t.orderId, t.cardHash, t.sequence, t.seller, t.buyer, t.host, 'XMR', t.priceAtomicXmr, t.feeBps, ADDITIONAL_MARKET_FEE.bps, ADDITIONAL_MARKET_FEE.address, t.lockUntilHeight, t.expiresHeight])); const custom = fixture({ hostFeeAddress: '8'.repeat(95) }); // Syntax-only candidate, no wallet/RPC claim. assert.notEqual(saleId(custom.terms), saleId({ ...custom.terms, hostFeeAddress: undefined })); assert.equal(apply(custom.state, [custom.lock]).sales[saleId(custom.terms)].terms.hostFeeAddress, '8'.repeat(95)); assert.throws(() => apply(custom.state, [{ ...custom.lock, terms: { ...custom.terms, hostFeeAddress: ADDITIONAL_MARKET_FEE.address } }]), /both trader/); assert.throws(() => apply(custom.state, [{ ...custom.lock, terms: { ...custom.terms, hostFeeAddress: undefined } }]), /both trader/); }); test('locked cards cannot escape through free transfers or any leg of an atomic trade', () => { const f = fixture(), locked = apply(f.state, [f.lock]); assert.throws(() => apply(locked, [gift(1)]), /locked/); assert.throws(() => apply(locked, [bundle(1)]), /locked/); assert.equal(apply(f.state, [gift(0)]).nfts[card].owner, attacker.owner); const trade = apply(f.state, [bundle(0)]); assert.equal(trade.nfts[card].owner, buyer.owner); assert.equal(trade.nfts[second].owner, seller.owner); // A gift before lock invalidates the seller's stale lock authorization. assert.throws(() => apply(f.state, [gift(0), f.lock]), /stale/); }); test('cancellation needs buyer and host, releases only after canonical depth and invalidates all old signatures', () => { const f = fixture(), locked = apply(f.state, [f.lock]); assert.throws(() => apply(locked, [{ ...f.cancel, buyerAuthorization: attacker.authorization(saleCancellationMessage(f.cancel as Extract)) }]), /host and buyer/); const cancelled = apply(locked, [f.cancel]); assert.equal(cancelled.nfts[card].owner, seller.owner); assert.equal(cancelled.nfts[card].sequence, 2); assert.equal(saleDecision(cancelled, f.terms, context).action, 'hold'); assert.equal(saleDecision(at(cancelled, 13), f.terms, context).action, 'refund'); assert.throws(() => apply(cancelled, [f.complete]), /No active/); assert.throws(() => apply(cancelled, [f.lock]), /stale/); assert.throws(() => apply(cancelled, [gift(0)]), /stale/); assert.throws(() => apply(cancelled, [gift(1)]), /stale/); assert.equal(apply(cancelled, [gift(2)]).nfts[card].owner, attacker.owner); }); test('expiry is permissionless exactly at expiry height, but never a website-clock-only release', () => { const f = fixture(), locked = apply(f.state, [f.lock]); assert.throws(() => apply(locked, [f.expire]), /not expired/); const before = at(locked, 19); assert.throws(() => apply(before, [f.complete]), /outside/); assert.throws(() => apply(before, [f.cancel]), /permissionless expiry/); assert.throws(() => apply(before, [gift(1)]), /locked/); // Explicit expiry transaction required, no implicit timer. const expired = apply(before, [f.expire]); assert.equal(expired.nfts[card].owner, seller.owner); assert.equal(expired.nfts[card].sequence, 2); assert.equal(saleDecision(expired, f.terms, context).action, 'hold'); assert.equal(saleDecision(at(expired, 22), f.terms, context).action, 'refund'); const last = apply(at(locked, 18), [f.complete]); assert.equal(last.sales[saleId(f.terms)].status, 'completed'); }); test('withheld lock has a strict admission deadline and missing lock refunds wait beyond that deadline', () => { const f = fixture(); assert.equal(saleDecision(at(f.state, 12), f.terms, context).action, 'hold'); assert.equal(apply(at(f.state, 11), [f.lock]).sales[saleId(f.terms)].lockHeight, 12); assert.throws(() => apply(at(f.state, 12), [f.lock]), /height window/); assert.equal(saleDecision(at(f.state, 14), f.terms, context).action, 'hold'); assert.equal(saleDecision(at(f.state, 15), f.terms, context).action, 'refund'); }); test('one canonical outcome wins: cancel/complete race rejects the whole invalid block, never partially updates state', () => { const f = fixture(), ready = at(apply(f.state, [f.lock]), 11), prior = structuredClone(ready); assert.throws(() => apply(ready, [f.complete, f.cancel]), /No active/); assert.throws(() => apply(ready, [f.cancel, f.complete]), /No active/); assert.deepEqual(ready, prior); assert.throws(() => apply(ready, [f.complete, f.complete]), /Duplicate/); const completed = apply(ready, [f.complete]), cancelled = apply(ready, [f.cancel]); assert.equal(completed.sales[saleId(f.terms)].status, 'completed'); assert.equal(cancelled.sales[saleId(f.terms)].status, 'cancelled'); // Reorg to a sibling outcome after a financial posting must quarantine rather than pay/refund again. assert.equal(saleDecision(at(cancelled, 14), f.terms, context, 'settled').action, 'review'); assert.equal(saleDecision(at(completed, 14), f.terms, context, 'refunded').action, 'review'); assert.equal(saleDecision(at(completed, 14), f.terms, context, 'review').action, 'review'); }); test('cross-market competing locks are exclusive and cancelled order IDs cannot be recycled under fresh terms', () => { const f = fixture(), otherMarket = fixture({ custodianId: randomUUID() }); const locked = apply(f.state, [f.lock]); assert.throws(() => apply(locked, [otherMarket.lock]), /locked/); const cancelled = apply(locked, [f.cancel]); const replay = fixture({ ...f.terms, sequence: 2, priceAtomicXmr: '2' }); assert.throws(() => apply(cancelled, [replay.lock]), /already used/); const nextOrder = fixture({ sequence: 2 }); assert.equal(apply(cancelled, [nextOrder.lock]).nfts[card].sequence, 3); }); test('policy/height bounds, immature issuance, sequence exhaustion and transaction limits reject safely', () => { const f = fixture(); assert.throws(() => saleContext({ ...context.policy, maxLockBlocks: 1 }), /window/); assert.throws(() => apply(f.state, Array(65).fill(f.lock)), /budget/); assert.throws(() => applySaleBlock({ ...f.state, height: Number.MAX_SAFE_INTEGER }, [], context), /exhausted/); const tooEarly = initialSaleState(f.state.nfts, 1); assert.throws(() => apply(tooEarly, [f.lock]), /Immature/); for (const sequence of [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER - 1]) { const huge = fixture({ sequence }); huge.state.nfts[card].sequence = sequence; assert.throws(() => apply(huge.state, [huge.lock]), /exhausted/); } assert.throws(() => apply(f.state, [fixture({ expiresHeight: 100 }).lock]), /window/); }); test('candidate drafts from real sandbox reservations without signing, changing balances or activating NFT delivery', async t => { const dir = await mkdtemp(join(tmpdir(), 'wikideck-sale-draft-')); const market = await CustodyMarket.open(dir, { feeBps: 250, card: () => ({ title: 'Draft test' }) }); t.after(async () => { market.close(); await rm(dir, { recursive: true, force: true }); }); const execute = (who: typeof buyer, action: CustodyAction) => { const unsigned: Omit = { version: 1, mode: 'sandbox', policy: CUSTODY_POLICY, network: NETWORK, chainId: CHAIN_ID, custodianId: market.id, actor: who.owner, publicKey: '', nonce: randomUUID(), expiresAt: Date.now() + 60000, action }; const publicKey = who.authorization('lookup').publicKey; unsigned.publicKey = publicKey; return market.execute({ ...unsigned, signature: who.authorization(custodyMessage(unsigned)).signature }); }; execute(buyer, { type: 'mock-credit' }); const listing = execute(seller, { type: 'list', cardHash: card, sequence: 0, priceAtomicXmr: '1', feeBps: 250 }) as CustodyListing; const order = execute(buyer, { type: 'reserve', listingId: listing.id, priceAtomicXmr: '1', feeBps: 250 }) as CustodyOrder; const before = market.audit(), options = { custodianId: market.id, host: host.owner, feeBps: 250, lockUntilHeight: 12, expiresHeight: 20 }; const terms = termsForCustodyOrder(order, context, options); assert.equal(terms.priceAtomicXmr, '1'); assert.equal(terms.orderId, order.id); assert.equal(terms.custodianId, market.id); assert.deepEqual(market.audit(), before); assert.equal(market.info().nftSettlement, false); assert.throws(() => termsForCustodyOrder({ ...order, authorAtomicXmr: '1' }, context, options), /allocations/); assert.throws(() => termsForCustodyOrder({ ...order, status: 'cancelled' }, context, options), /awaiting-lock/); assert.throws(() => termsForCustodyOrder({ ...order, hostFeeAddress: 'different-recipient' }, context, options)); }); test('enumerated adversarial histories preserve exclusivity and deterministic replay across snapshot restores', () => { const f = fixture(), start = apply(f.state, [f.lock]); let explored = 0; const explore = (state: SaleState, depth: number) => { if (depth === 0) return; const choices = [[], [f.complete], [f.cancel], [f.expire], [gift(state.nfts[card].sequence)], [bundle(state.nfts[card].sequence)]]; for (const txs of choices) { const before = structuredClone(state); try { const next = apply(state, txs); assert.deepEqual(next, apply(JSON.parse(JSON.stringify(state)), txs)); const sale = next.sales[saleId(f.terms)]; if (sale.status === 'locked') assert.equal(next.nfts[card].owner, seller.owner); assert.equal(!!next.locks[card], sale.status === 'locked'); explored++; explore(next, depth - 1); } catch (e) { if (e instanceof assert.AssertionError) throw e; } assert.deepEqual(state, before); } }; // Reach the exact expiry boundary too; short horizons mostly contain correctly rejected actions. explore(start, 12); assert.ok(explored > 100, `Explored only ${explored} accepted transitions`); }); test('candidate state commitment binds ownership, lock authority, terminal outcomes and order-use tombstones', () => { const f = fixture(), state = apply(f.state, [f.lock]), root = saleStateRoot(state, context); assert.equal(root, saleStateRoot(JSON.parse(JSON.stringify(state)), context)); for (const mutate of [ (s: SaleState) => { s.nfts[card].sequence++; }, (s: SaleState) => { s.sales[saleId(f.terms)].terms.host = attacker.owner; }, (s: SaleState) => { s.sales[saleId(f.terms)].status = 'cancelled'; }, (s: SaleState) => { delete s.locks[card]; }, (s: SaleState) => { s.orders = {}; }, ]) { const copy = structuredClone(state); mutate(copy); assert.notEqual(saleStateRoot(copy, context), root); } });