use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResourceDim {
GpuSeconds,
GridCarbonGramsCo2,
PeerVerifierMillis,
CorpusBitsAdded,
KnowledgeSpillover,
FxVolatilityDelta,
AuctionDelay,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct ExternalityProfile {
pub dimensions: BTreeMap<ResourceDim, OpaqueSignedClaim>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpaqueSignedClaim {
pub signed_bytes: Vec<u8>,
pub signature: Vec<u8>,
}
#[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 {
pub winners: Vec<(String, u64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VickreyPayload {
pub auction: PostedAuction,
pub bids: Vec<AgentBid>,
pub match_result: MatchResult,
}
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");
}
#[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");
}
#[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);
}
}