Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-monetize-guest/tests/published_embed_agrees_with_the_head.rs

//! ★ **The monetize-embed gunnar-server links is the monetize-embed monetize signs with**
//! (gunnar.rs audit 2026-10-12, S14).
//!
//! gunnar-server and gunnar-grpc resolve `monetize-embed` from crates.io (0.1.3, the
//! `version` requirement their manifests explain), while this guest — and monetize-server —
//! build the HEAD by path. The two copies sit side by side in gunnar's `Cargo.lock`. Today the
//! signed forms agree; the day monetize changes `ticket_message`, `fact_message`, the snapshot
//! or the go-ahead form and nobody publishes, every ticket gunnar mints and every fact it
//! checks would fail SILENTLY in the field.
//!
//! This test builds every signed form both ways from the same values and asserts the bytes
//! are equal, and that the purpose words gunnar spells locally (gunnar-grpc
//! `service/billing.rs`: `PURPOSE_CANCEL`, `PURPOSE_ESTATE`, because 0.1.3 predates them)
//! are the head's. It fails on the first drift: publish monetize-embed from the head (and
//! `cargo update -p monetize-embed` here) before such a change merges.

use std::collections::BTreeMap;

use monetize_embed as head;
use monetize_embed_published as published;

fn fact_json(issued: Option<u64>) -> serde_json::Value {
    serde_json::json!({
        "tenant": "@test2-gunnar-rs/nina",
        "plan": "gunnar/nina/2026-10-12+pack_bytes=10737418240",
        "state": "Paid",
        "paid_until_unix_ms": 1_793_000_000_000u64,
        "caps": { "pack_bytes": 10_737_418_240u64, "lfs_bytes": 1_073_741_824u64 },
        "source": "payment:stripe/gunnar-test2:pi_123",
        "signature": [1, 2, 3],
        "issued_unix_ms": issued,
    })
}

fn both_facts(issued: Option<u64>) -> (head::EntitlementFact, published::EntitlementFact) {
    let h: head::EntitlementFact =
        serde_json::from_value(fact_json(issued)).expect("the head reads the fact");
    // Through the head's own serialisation: a field the head added and 0.1.3 lacks shows up
    // as a different signed form below, never as a quiet drop here.
    let p: published::EntitlementFact = serde_json::from_value(serde_json::to_value(&h).unwrap())
        .expect("0.1.3 reads the head's fact");
    (h, p)
}

#[test]
fn a_fact_signs_the_same_bytes_in_both_copies() {
    for issued in [None, Some(1_792_000_000_000u64)] {
        let (h, p) = both_facts(issued);
        assert_eq!(
            head::signing::fact_message(&h),
            published::signing::fact_message(&p),
            "V1 fact form drifted (issued {issued:?})"
        );
        if issued.is_some() {
            assert_eq!(
                head::signing::fact_message_issued(&h),
                published::signing::fact_message_issued(&p),
                "V2 fact form drifted"
            );
        }
        assert_eq!(
            head::signing::snapshot_message(7, std::slice::from_ref(&h)),
            published::signing::snapshot_message(7, std::slice::from_ref(&p)),
            "snapshot form drifted"
        );
    }
}

#[test]
fn a_ticket_signs_the_same_bytes_in_both_copies() {
    let caps = BTreeMap::from([("pack_bytes".to_string(), 10_737_418_240u64)]);
    for purpose in [
        head::ticket::PURPOSE_RENEW,
        head::ticket::PURPOSE_ORDER,
        head::ticket::PURPOSE_READ,
        head::ticket::PURPOSE_CANCEL,
        head::ticket::PURPOSE_ESTATE,
    ] {
        let h = head::ticket::ActorTicket::unsigned(
            "gunnar",
            "gunnar",
            "nina",
            purpose,
            "nonce-1",
            1_792_000_000_000,
            60_000,
        )
        .covering(caps.clone());
        let p = published::ticket::ActorTicket::unsigned(
            "gunnar",
            "gunnar",
            "nina",
            purpose,
            "nonce-1",
            1_792_000_000_000,
            60_000,
        )
        .covering(caps.clone());
        assert_eq!(
            head::ticket::ticket_message(&h),
            published::ticket::ticket_message(&p),
            "ticket form drifted ({purpose})"
        );
    }
    assert_eq!(
        head::ticket::MAX_LIFE_MS,
        published::ticket::MAX_LIFE_MS,
        "a ticket's life drifted"
    );
}

/// gunnar-grpc `service/billing.rs` spells these two because 0.1.3 has no constant for them;
/// the verifier compares the word, so the literal is the contract.
#[test]
fn the_purpose_words_gunnar_spells_itself_are_the_heads() {
    assert_eq!(
        head::ticket::PURPOSE_CANCEL,
        "cancel",
        "gunnar-grpc billing.rs PURPOSE_CANCEL"
    );
    assert_eq!(
        head::ticket::PURPOSE_ESTATE,
        "estate",
        "gunnar-grpc billing.rs PURPOSE_ESTATE"
    );
    assert_eq!(
        (
            head::ticket::PURPOSE_RENEW,
            head::ticket::PURPOSE_ORDER,
            head::ticket::PURPOSE_READ
        ),
        (
            published::ticket::PURPOSE_RENEW,
            published::ticket::PURPOSE_ORDER,
            published::ticket::PURPOSE_READ
        )
    );
}

#[test]
fn a_go_ahead_and_the_canonical_form_agree() {
    let go = serde_json::json!({ "v": 1, "signer": head::signing::GO_AHEAD_SIGNER_MONETIZE, "target_sectors": 41_943_040u64, "nonce": "n-1", "issued_unix_ms": 1_792_000_000_000i64, "signature": "" });
    let h: head::signing::GoAhead = serde_json::from_value(go.clone()).unwrap();
    let p: published::signing::GoAhead =
        serde_json::from_value(serde_json::to_value(&h).unwrap()).unwrap();
    assert_eq!(
        head::signing::go_ahead_message(&h),
        published::signing::go_ahead_message(&p),
        "go-ahead form drifted"
    );
    assert_eq!(
        head::signing::GO_AHEAD_SIGNER_MONETIZE,
        published::signing::GO_AHEAD_SIGNER_MONETIZE
    );

    let v = serde_json::json!({ "b": [3, {"z": 1, "a": "x"}], "a": null, "c": "\u{e5}\"\n" });
    let (mut hs, mut ps) = (String::new(), String::new());
    head::signing::canonical_json(&v, &mut hs);
    published::signing::canonical_json(&v, &mut ps);
    assert_eq!(hs, ps, "canonical JSON drifted");
    let bytes = [0u8, 1, 2, 250, 251, 252, 253, 254, 255];
    assert_eq!(
        head::signing::base64_encode(&bytes),
        published::signing::base64_encode(&bytes),
        "the signature alphabet drifted"
    );
}