nucleus-substrate-core 0.1.0

Categorical core of the Nucleus substrate: Session (the agent action), Projection (the verifiable record functor — Identity, Capability, Flow, Economic, …), and Receipt (the Ed25519-signed colimit). Lightweight, no runtime deps. See docs/architecture/substrate.md.
Documentation
//! Pigouvian-VCG mechanism types.
//!
//! These are the wire shapes the auction-hub server emits and the
//! mechanism-vcg lifter (`nucleus-mechanism-vcg`) consumes when
//! constructing the body of a [`Projection::Economic`] variant.
//!
//! Lean theorems backing the mechanism live in `formal/Nucleus/Auctions/`:
//!
//!   * `IntegerVcgTruthful.vickrey_truthful` — single-good
//!     strategy-proofness.
//!   * `VcgPigouTruthful.pigou_vickrey_truthful` — Pigouvian-VCG
//!     truthfulness.
//!   * `PigouvianVcgSequential.sequential_welfare_bounded_above` —
//!     welfare bound.
//!
//! Aeneas-extracted µUSD parity is enforced by the proptest in
//! `crates/nucleus-econ-kernels/tests/pigou_parity.rs`.
//!
//! [`Projection::Economic`]: super::super::Projection::Economic

use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

// ── Resource dimensions for Pigouvian externalities ───────────

/// One axis of externality the auction internalizes. Each variant
/// captures a measurable externality and an oracle that signs claims
/// about it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResourceDim {
    /// GPU compute time consumed, in micro-seconds.
    GpuSeconds,
    /// Grid carbon intensity × electrical energy, in micro-grams CO₂-eq.
    GridCarbonGramsCo2,
    /// Peer-verifier CPU / I/O time imposed on the witness federation
    /// when this call's lineage edges get verified, in milliseconds.
    PeerVerifierMillis,
    /// Bits added to the corpus when this call's artifact is recorded.
    CorpusBitsAdded,
    /// **Negative externality** (= positive spillover): knowledge
    /// produced and made available to other agents.
    KnowledgeSpillover,
    /// FX volatility imposed on subsequent FX-denominated bids.
    FxVolatilityDelta,
    /// Auction-clearing delay imposed on downstream auctions,
    /// in milliseconds.
    AuctionDelay,
}

// ── Externality profile ───────────────────────────────────────

/// One agent's claims about the externalities they would impose if
/// awarded a given auction. Each claim is signed by a trusted oracle;
/// the hub verifies signatures before the VCG path consumes them.
///
/// Profiles are **opaque to the SDK** — clients construct them from
/// raw signed-claim bytes obtained out-of-band from an oracle
/// service. The hub deserializes them via the same struct shape.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct ExternalityProfile {
    /// Map from dimension to the signed claim for that dimension.
    pub dimensions: BTreeMap<ResourceDim, OpaqueSignedClaim>,
}

/// A signed externality claim, opaque from the SDK's perspective.
/// The hub deserializes the inner `signed_bytes` against the oracle's
/// verifying key; SDK consumers pass through whatever bytes they
/// received from their oracle.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpaqueSignedClaim {
    /// Canonical bytes of the claim payload (oracle-defined shape).
    pub signed_bytes: Vec<u8>,
    /// Ed25519 signature over `signed_bytes`.
    pub signature: Vec<u8>,
}

// ── Auction lifecycle ─────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PostedAuction {
    pub auction_id: String,
    pub required_capabilities: BTreeSet<String>,
    pub reward_micro_usd: u64,
    pub pigouvian_rates: Vec<(ResourceDim, u64)>,
    pub scale: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AgentBid {
    pub agent_spiffe_id: String,
    pub auction_id: String,
    pub effective_value_micro_usd: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub externality_profile: Option<ExternalityProfile>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MatchResult {
    pub auction_id: String,
    pub winner_spiffe_id: Option<String>,
    pub clearing_price_micro_usd: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VcgMatchResult {
    /// (winner SPIFFE id, Clarke-pivot payment in µUSD)
    pub winners: Vec<(String, u64)>,
}

// ── Helper: pack a cleared auction into an Economic projection ──

/// Wire payload of the Economic projection variant when a single-good
/// Vickrey match has cleared. Roughly mirrors the hub's `/match`
/// response plus the bid set that produced it.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VickreyPayload {
    pub auction: PostedAuction,
    pub bids: Vec<AgentBid>,
    pub match_result: MatchResult,
}

/// Build a [`Projection::Economic`] body from a cleared single-good
/// Vickrey auction. Callers will typically wrap this in a `Receipt`
/// with the relevant `Session`.
///
/// [`Projection::Economic`]: crate::Projection::Economic
pub fn vickrey_projection_body(
    auction: PostedAuction,
    bids: Vec<AgentBid>,
    match_result: MatchResult,
) -> serde_json::Value {
    let payload = VickreyPayload {
        auction,
        bids,
        match_result,
    };
    serde_json::to_value(payload).expect("VickreyPayload serializes deterministically")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Projection, Receipt, Session};

    fn dummy_auction() -> PostedAuction {
        PostedAuction {
            auction_id: "a1".into(),
            required_capabilities: BTreeSet::new(),
            reward_micro_usd: 1_000_000,
            pigouvian_rates: vec![(ResourceDim::GpuSeconds, 100)],
            scale: 1_000_000,
        }
    }

    fn dummy_match() -> MatchResult {
        MatchResult {
            auction_id: "a1".into(),
            winner_spiffe_id: Some("spiffe://test/agent".into()),
            clearing_price_micro_usd: 250_000,
        }
    }

    #[test]
    fn vickrey_payload_round_trips_serde() {
        let payload = VickreyPayload {
            auction: dummy_auction(),
            bids: vec![],
            match_result: dummy_match(),
        };
        let json = serde_json::to_string(&payload).unwrap();
        let back: VickreyPayload = serde_json::from_str(&json).unwrap();
        assert_eq!(back.auction.auction_id, "a1");
        assert_eq!(back.match_result.clearing_price_micro_usd, 250_000);
    }

    #[test]
    fn projection_body_helper_packs_into_economic_variant() {
        let body = vickrey_projection_body(dummy_auction(), vec![], dummy_match());
        let projection = Projection::Economic(body);
        assert_eq!(projection.kind(), "economic");
    }

    /// **End-to-end**: pack a cleared auction into the categorical
    /// Receipt, sign it, verify offline. This is the canonical
    /// "auction-hub emits a Receipt" code path.
    #[test]
    fn cleared_auction_round_trips_through_receipt() {
        let sk = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
        let session = Session {
            session_id: "spiffe://test/auction-hub".into(),
            issuer_kid: "test-kid".into(),
            issued_at_micros: 1_717_000_000_000_000,
            parent_chain: vec![],
        };
        let body = vickrey_projection_body(dummy_auction(), vec![], dummy_match());
        let receipt = Receipt::sign(session, vec![Projection::Economic(body)], &sk);
        let vk: [u8; 32] = sk.verifying_key().to_bytes();
        receipt.verify(&vk).expect("cleared-auction Receipt verifies");
    }

    /// **Wire-shape regression** — assert the JSON layout downstream
    /// consumers will see. If this ever changes, that's a SemVer
    /// break for substrate-core.
    #[test]
    fn projection_economic_wire_format_includes_auction_id() {
        let body = vickrey_projection_body(dummy_auction(), vec![], dummy_match());
        let projection = Projection::Economic(body);
        let v = serde_json::to_value(&projection).unwrap();
        assert_eq!(v["kind"], "economic");
        assert_eq!(v["body"]["auction"]["auction_id"], "a1");
        assert_eq!(v["body"]["match_result"]["clearing_price_micro_usd"], 250_000);
    }
}