diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index d8e5859..b3ad53e 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -35,7 +35,8 @@ infrastructure, not as a general privacy app: The recorded evidence view proves the contract lifecycle, settlement, and public audit path. -## 1. Showcase (30s) +## 1. +Showcase (30s) - Opening: escrow-backed sealed auctions on Stellar - Mainnet proof card: settled round 1 on real XLM (link to stellar.expert) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 65ce850..6a139fc 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,6 +1,6 @@ # Deploy & environment variables -Sub Rosa **does not require a committed `.env` file**. Secrets stay out of git; you inject them where each layer runs. +Sub Rosa ** does not require a committed `.env` file**. Secrets stay out of git; you inject them where each layer runs. ## Three layers diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 7b24cf2..e650a9a 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -3,7 +3,7 @@ Honest boundaries for the current prototype and submission materials. No hidden fallbacks. -## Network scope +##Network scope | Proof | Network | What it shows | | --- | --- | --- | diff --git a/services/agent/README.md b/services/agent/README.md new file mode 100644 index 0000000..726da2d --- /dev/null +++ b/services/agent/README.md @@ -0,0 +1,68 @@ +# @sub-rosa/agent + +Autonomous sealed-bid agents. Each agent holds a principal-signed session +mandate (scoped caps), pays the x402 appraisal API per call, sizes a bid from +the appraisal, and commits over Soroban RPC with its session key — no relayer, +no mock. + +## Guard rails + +Every `runBidderAgent` call enforces three layers of off-chain guards: + +| Guard | Check | Error | When | +|-------|-------|-------|------| +| **Mandate signature** | Principal Ed25519 sig over canonical payload | `MandateError` | Before appraisal | +| **Appraisal spend cap** | Quoted price ≤ `appraisalPriceStroops`, cumulative ≤ `maxAppraisalSpendStroops` | `MandateCapError` | Before appraisal | +| **Bid mandate caps** | `bid ≤ maxBid`, `bid ≤ escrow ≤ maxEscrow` | `MandateCapError` | After appraisal | +| **Balance** | Session account SAC balance ≥ escrow | `InsufficientBalanceError` | After appraisal | + +The balance check simulates a `balance(addr)` call against the configured +`usdcSacId` SAC contract using the RPC endpoint. This prevents the agent from +paying gas for a `commit` that would revert on-chain due to insufficient +escrow token balance. + +Guards that run **before** appraisal (mandate signature, appraisal spend cap) +avoid wasting the appraisal fee. Guards that run **after** appraisal (bid +mandate caps, balance) depend on the appraisal result and prevent wasted gas +on a failing commit. + +## Usage + +```ts +import { createSessionMandate, runBidderAgent } from "@sub-rosa/agent"; + +const { mandate, sessionSecret } = createSessionMandate({ + principalSecret: "...", + contractId: "...", + roundId: 1n, + itemRef: "sub-rosa://rfp/123", + basePriceUsdc: 500, + maxBidStroops: 7_000_000_000n, // 700 USDC + maxEscrowStroops: 7_000_000_000n, // 700 USDC + maxAppraisalSpendStroops: 10_000_000n, + appraisalPriceStroops: 1_000_000n, + commitDeadline: Math.floor(Date.now() / 1000) + 3600, +}); + +const result = await runBidderAgent({ + mandate, + sessionSecret, + rpcUrl: "https://soroban-testnet.stellar.org", + networkPassphrase: "Test SDF Network ; September 2015", + appraisalUrl: "http://localhost:3000/appraise", + auditorPubkey: auditorPublicKey, + revealRound: 12345, + attributes: { quality: 88, demand: 82, scarcity: 92, risk: 12 }, + usdcSacId: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", +}); +``` + +## API + +See `src/index.ts` for the full export surface. Key exports: + +- `runBidderAgent(config)` — autonomous bid: verify → appraise → seal → commit +- `createSessionMandate(params)` — issue a principal-signed session mandate +- `assertBidWithinMandate(mandate, bid, escrow)` — refuse cap violations +- `assertSufficientBalance(escrow, balance)` — refuse under-funded escrow +- `InsufficientBalanceError` — typed error with `.escrow` and `.balance` fields diff --git a/services/agent/scripts/agents-e2e.ts b/services/agent/scripts/agents-e2e.ts index c1f14f2..8fb0d7b 100644 --- a/services/agent/scripts/agents-e2e.ts +++ b/services/agent/scripts/agents-e2e.ts @@ -304,6 +304,7 @@ async function main() { auditorPubkey: auditor.publicKey, revealRound, attributes: plan.attributes, + usdcSacId: usdcSac, x402Network: X402_NETWORK as `${string}:${string}`, log, }); diff --git a/services/agent/src/bidder.ts b/services/agent/src/bidder.ts index 70ed125..7ca3469 100644 --- a/services/agent/src/bidder.ts +++ b/services/agent/src/bidder.ts @@ -8,7 +8,7 @@ import { Keypair } from "@stellar/stellar-sdk"; import type { Network, SettleResponse } from "@x402/core/types"; import type { Appraisal, AppraisalAttributes, AppraisalRequest } from "@sub-rosa/appraisal-api"; import { createPaidFetch } from "@sub-rosa/appraisal-api"; -import { SubRosaClient } from "@sub-rosa/sdk"; +import { SubRosaClient, createSacBalanceReader } from "@sub-rosa/sdk"; import { generateNonce, quicknet, @@ -19,6 +19,7 @@ import { import { assertAppraisalSpendAllowed, assertBidWithinMandate, + assertSufficientBalance, bidFromAppraisal, stroopsToUsdc, verifySessionMandate, @@ -38,6 +39,9 @@ export interface BidderAgentConfig { revealRound: number; /** Appraisal attributes — each agent can supply its own private view. */ attributes: AppraisalAttributes; + /** SAC contract id for the escrow token (e.g. USDC on testnet). + * Required for the pre-flight balance check. */ + usdcSacId: string; x402Network?: Network; drand?: DrandClient; log?: (msg: string) => void; @@ -112,7 +116,19 @@ export async function runBidderAgent(config: BidderAgentConfig): Promise { ); }); +test("NEGATIVE: agent rejects escrow exceeding session balance", () => { + assert.throws( + () => assertSufficientBalance(usdcToStroops(200), usdcToStroops(50)), + InsufficientBalanceError, + ); +}); + test("ON-CHAIN RULE: value > escrow → valid=false at reveal (documented)", () => { const escrow = 50n; const value = 80n; diff --git a/services/agent/src/mandate.test.ts b/services/agent/src/mandate.test.ts index 5579e03..cb1471e 100644 --- a/services/agent/src/mandate.test.ts +++ b/services/agent/src/mandate.test.ts @@ -6,8 +6,10 @@ import { Keypair } from "@stellar/stellar-sdk"; import { assertAppraisalSpendAllowed, assertBidWithinMandate, + assertSufficientBalance, bidFromAppraisal, createSessionMandate, + InsufficientBalanceError, MandateCapError, MandateError, usdcToStroops, @@ -83,3 +85,20 @@ test("bidFromAppraisal clamps to mandate maxBid", () => { assert.equal(bidValue, usdcToStroops(40)); assert.equal(escrow, usdcToStroops(40)); }); + +test("assertSufficientBalance passes when escrow <= balance", () => { + assert.doesNotThrow(() => + assertSufficientBalance(usdcToStroops(50), usdcToStroops(100)), + ); + // Equal is fine too. + assert.doesNotThrow(() => + assertSufficientBalance(usdcToStroops(100), usdcToStroops(100)), + ); +}); + +test("assertSufficientBalance throws when escrow exceeds balance", () => { + assert.throws( + () => assertSufficientBalance(usdcToStroops(150), usdcToStroops(100)), + InsufficientBalanceError, + ); +}); diff --git a/services/agent/src/mandate.ts b/services/agent/src/mandate.ts index 6c34fd1..c081bfd 100644 --- a/services/agent/src/mandate.ts +++ b/services/agent/src/mandate.ts @@ -47,6 +47,19 @@ export interface SessionMandate extends SessionMandatePayload { export class MandateError extends Error {} export class MandateCapError extends MandateError {} +/** Thrown when the session account lacks sufficient token balance to cover + * the escrow required for a bid. Agent-side guard — prevents wasted fees + * from submitting a commit that would fail on-chain. */ +export class InsufficientBalanceError extends MandateError { + constructor( + message: string, + readonly escrow: bigint, + readonly balance: bigint, + ) { + super(message); + } +} + const canonical = (value: unknown): string => { if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; if (value && typeof value === "object") { @@ -197,6 +210,22 @@ export function assertBidWithinMandate( } } +/** Refuse an escrow that exceeds the session account's token balance. + * Agent-side pre-flight guard — prevents committing funds the account + * does not hold. */ +export function assertSufficientBalance( + escrow: bigint, + balance: bigint, +): void { + if (escrow > balance) { + throw new InsufficientBalanceError( + `escrow ${escrow} exceeds session balance ${balance}`, + escrow, + balance, + ); + } +} + /** Size bid + escrow from a paid appraisal, clamped to the mandate. */ export function bidFromAppraisal( suggestedMaxBidUsdc: number,