Intent
Reference documentation for the Zypp intent data model — the core type that flows through the entire pipeline.
Intent Structure
pub struct Intent {
pub id: uuid::Uuid, // Stable identity for retry correlation
pub content_hash: u64, // FNV-1a fingerprint of the serialized payload
pub intent_type: IntentType, // Payment | Ticket | Action
pub payload: IntentPayload, // Typed payload (see below)
pub trust_tier: TrustTier, // Unsigned | Signed
pub envelope: Option<CryptographicEnvelope>, // Signature + nonce
}
Fields
| Field | Type | Description |
|---|---|---|
id | Uuid | Stable identity for retry correlation and idempotency keys |
content_hash | u64 | FNV-1a 64-bit hash of the serialized payload. Used for local dedup — equal hash = same logical action. Computed automatically in Intent::new() |
intent_type | IntentType | Closed enum — Payment, Ticket, or Action |
payload | IntentPayload | Variant-specific payload (see below) |
trust_tier | TrustTier | Security tier: Unsigned (fingerprint-only) or Signed (signature + nonce) |
envelope | Option<CryptographicEnvelope> | Present when trust_tier == Signed. Bundles signature, public key, and per-key monotonic nonce |
IntentType
pub enum IntentType {
Payment,
Ticket,
Action,
}
A closed enum — no arbitrary string passthrough. Adding a new variant requires an SDK version bump.
IntentPayload
pub enum IntentPayload {
Payment(PaymentPayload), // Fixed schema
Ticket { payload: FreeFormPayload, signatures: DualSignatureEnvelope }, // Dual-signature
Action(FreeFormPayload), // Free-form
}
PaymentPayload (SDK-fixed)
pub struct PaymentPayload {
pub amount: u64, // lamports or smallest token unit
pub asset: Vec<u8>, // Mint account address (32 bytes)
pub recipient: Vec<u8>, // Recipient wallet address (32 bytes)
}
Fixed schema owned strictly by the SDK to guarantee payment safety.
FreeFormPayload (dev-owned)
pub struct FreeFormPayload {
pub kind: String, // e.g. "move_player", "concert_vip_redeem"
pub data: Vec<u8>, // Serialized application-specific fields
}
The SDK does not parse or validate data. The developer defines the schema.
TrustTier
pub enum TrustTier {
Unsigned, // No cryptographic signature; tracked via device/session fingerprint
Signed, // Cryptographically signed; automatically bundles replay protection
}
- Unsigned: No signature required. Suitable for low-stakes actions like move_avatar or ping.
- Signed: Ed25519 signature with per-key monotonic nonce. Mandatory for Payment intents. Recommended for high-stakes actions.
CryptographicEnvelope
pub struct CryptographicEnvelope {
pub signature: Vec<u8>, // Ed25519 signature bytes
pub public_key: Vec<u8>, // Signer's public key
pub nonce: u64, // Per-key monotonically increasing counter
}
This envelope is verified by the sync engine before allowing the intent into the sync pipeline. The nonce is scoped to the public key — two distinct devices sharing a counter value would be mutually replayable, so per-key monotonicity prevents cross-device replay.
DualSignatureEnvelope (Ticket-specific)
pub struct DualSignatureEnvelope {
pub issuer_signature: Vec<u8>, // Proof of legitimacy
pub holder_signature: Option<Vec<u8>>, // Proof of execution
pub nonce: u64,
}
Tickets carry two separate signature fields:
- Issuer signature: Proves the ticket was legitimately created by the event issuer
- Holder signature: Proves the device performed the redemption
Signing order is enforced by guards:
- Issuer signature must be set first
- Holder signature requires non-empty issuer signature
Content Hash
Every intent gets a deterministic FNV-1a 64-bit content fingerprint computed from the serialized payload only:
// types.rs
let serialized = bincode::serde::encode_to_vec(&payload, bincode::config::standard()).unwrap();
let content_hash = fnv1a(&serialized);
The hash covers only IntentPayload — not intent_type, trust_tier, envelope, or id. This is by design: dedup keys on the logical action, not on signature material. Two intents with identical payloads but different trust tiers produce the same content_hash.
Intent Lifecycle
// Create an Intent
let intent = Intent::new(
uuid::Uuid::new_v4(),
IntentType::Action,
IntentPayload::Action(FreeFormPayload {
kind: "swap_tokens".into(),
data: bincode::serde::encode_to_vec(&swap_params, bincode::config::standard()).unwrap(),
}),
TrustTier::Signed,
None, // envelope completed downstream
);
// content_hash is computed automatically from the payload
QueueEntry
pub struct QueueEntry {
pub intent: Intent,
pub status: QueueStatus,
pub created_at: u64,
pub updated_at: u64,
}
Each intent in the queue is wrapped in a QueueEntry that tracks its lifecycle status and timestamps. Status transitions are enforced at the type level:
impl QueueEntry {
pub fn mark_provisional(&mut self); // Pending → SyncedProvisional
pub fn finalize(&mut self); // SyncedProvisional → SyncedFinalized
pub fn fail(&mut self, stage, reason); // Any → Failed (except SyncedFinalized)
}
FailureStage
pub enum FailureStage {
IntentCreation,
IntegrityCheck,
DeliveryValidation,
Settlement,
}
RejectionReason
pub enum RejectionReason {
DuplicateInLocalQueue,
SignatureInvalid,
TamperedPayload,
ValidationError { code: u32, message: String, details: Option<Vec<u8>> },
L1Reversion,
}
Next Steps
- Understand the Queue for lifecycle and state transitions
- Configure Settlement routing
- See the type design document in
rust-core/TYPES_DESIGN.mdfor detailed rationale