Blockchain Architecture (Sui)
This section describes how the platform uses Sui’s object‑centric execution model to encode economic state, access control, and verifiable outcomes. It outlines our Move packages, object types and abilities, shared versus owned semantics, event contracts, upgrade strategy, and the transaction flows that power the experience.
Why Sui for this platform
Sui’s object model and execution are a strong fit for a media system that must maintain fine‑grained access and attribution without centralized servers. In Sui, Move code manipulates first‑class objects with explicit ownership and abilities, enabling us to represent content, access entitlements, bounties, and reputation as data the client can fetch and verify directly. Sui’s enhancements to Move (e.g., object ownership, dynamic fields, eventing, and package upgrades) provide the primitives we need to scale safely.
Packages and modules (overview)
- Content & Licensing (Kiosk v2 with TransferPolicy)
- We adopt Sui's Kiosk v2 standard with TransferPolicy to govern minting, listing, royalties, and transfer rules for content objects. Policies enforce floor prices, perpetual royalties through on-chain rules, and custom transfer restrictions, linking objects to Walrus blob IDs for decentralized storage.
- Access Entitlements
- Minted per reader as owned objects representing per‑article access or subscription windows. Designed for O(1) verification by clients: ownership + time window.
- Bounty Escrow
- Shared objects representing active bounties with deposits, terms, and timers. Supports accept, reject with slashing, and timed resolve. Emits events consumed by clients.
- Reputation
- Pseudonymous score object per contributor. Updates are triggered only by finalized outcomes (e.g., bounty resolution). Designed for future threshold proofs.
- Market & Resolution Receipts
- Minimal market interface to link stories to markets and record final outcomes. Resolution receipts can include enclave attestations and zk proofs for on‑chain verification.
Object taxonomy and abilities
- Owned objects
Entitlement,Reputation(per account), and certainContentderivatives are owned. Owned objects allow single‑principal mutation and simple verification.
- Shared objects
Bounty,Market, andKioskare shared where multi‑party interaction is required. Shared objects route through consensus ordering; owned‑only flows can take Sui’s direct fast path.
- Dynamic fields
- Where association lists are needed (e.g., mapping content to markets), dynamic fields avoid large monolithic structs and let us append safely.
- Events
- Each module emits strictly versioned events that clients use to build local indexes, maintaining determinism and idempotence.
Transaction flows and signatures
Sui's transaction lifecycle enables fast, client‑assembled transactions for owned‑object flows, and consensus sequencing for shared‑object updates. We use programmable transaction blocks (PTBs) to compose multi‑step operations that remain atomic from the user's point of view, leveraging command chaining and gas optimization patterns.
Sponsored transactions and zkLogin
- Users may authenticate with wallets or via zkLogin using ephemeral keys and OIDC providers. When balances are low, a sponsor can pay gas through sponsored transactions by co‑signing while the user signs the intent. Both signatures are validated by the network.
Owned‑object fast path vs shared‑object consensus
- Entitlement purchases and reputation reads involve only owned objects and can follow Sui’s direct fast path once certified.
- Bounty updates and market resolutions involve shared objects and therefore pass through consensus ordering for safety before execution.
Module sketches and invariants
Entitlements Module
module pml::entitlements {
use sui::clock::{Self, Clock};
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::event;
/// Entitlement object - non-transferable access pass
struct Entitlement has key {
id: UID,
content_id: ID,
reader: address,
tier: u8,
valid_from: u64,
valid_until: u64
}
/// Purchase access to content
public entry fun purchase_access(
content_id: ID,
payment: Coin<SUI>,
duration_days: u64,
clock: &Clock,
ctx: &mut TxContext
) {
let reader = tx_context::sender(ctx);
let now = clock::timestamp_ms(clock);
let entitlement = Entitlement {
id: object::new(ctx),
content_id,
reader,
tier: 0,
valid_from: now,
valid_until: now + (duration_days * 86400 * 1000)
};
transfer::transfer(entitlement, reader);
transfer::public_transfer(payment, @treasury);
}
}
Kiosk v2 and TransferPolicy Integration
module pml::content_kiosk {
use sui::kiosk::{Self, Kiosk, KioskOwnerCap};
use sui::transfer_policy::{Self, TransferPolicy, TransferPolicyCap};
use kiosk::royalty_rule;
use sui::package::{Self, Publisher};
struct ContentNFT has key, store {
id: UID,
title: String,
author: address,
walrus_blob_id: vector<u8>,
content_hash: vector<u8>
}
struct CONTENT_KIOSK has drop {}
fun init(witness: CONTENT_KIOSK, ctx: &mut TxContext) {
let publisher = package::claim(witness, ctx);
transfer::public_share_object(publisher);
}
public fun create_policy_with_royalties(
publisher: &Publisher,
royalty_bps: u16,
min_price: u64,
ctx: &mut TxContext
): (TransferPolicy<ContentNFT>, TransferPolicyCap<ContentNFT>) {
let (policy, cap) = transfer_policy::new<ContentNFT>(publisher, ctx);
royalty_rule::add(&mut policy, &cap, royalty_bps, min_price);
(policy, cap)
}
}
Bounty Escrow Module
module pml::bounty {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::clock::{Self, Clock};
use sui::table::{Self, Table};
struct Bounty has key {
id: UID,
journalist: address,
reward: Balance<SUI>,
requirements_hash: vector<u8>,
deadline: u64,
submissions: Table<address, Submission>,
status: u8
}
struct Submission has store {
source: address,
encrypted_hash: vector<u8>,
walrus_id: vector<u8>,
timestamp: u64
}
struct BountyResolution {
bounty_id: ID,
winner: Option<address>,
should_slash: bool
}
public entry fun create_bounty(
reward: Coin<SUI>,
requirements_hash: vector<u8>,
duration_days: u64,
clock: &Clock,
ctx: &mut TxContext
) {
let bounty = Bounty {
id: object::new(ctx),
journalist: tx_context::sender(ctx),
reward: coin::into_balance(reward),
requirements_hash,
deadline: clock::timestamp_ms(clock) + (duration_days * 86400 * 1000),
submissions: table::new(ctx),
status: 0
};
transfer::share_object(bounty);
}
}
Reputation Module
module pml::reputation {
friend pml::bounty;
struct Reputation has key {
id: UID,
score: u64,
verified_submissions: u64,
rejected_count: u64,
trust_level: u8
}
public(friend) fun update_score(
rep: &mut Reputation,
accepted: bool,
points: u64
) {
if (accepted) {
rep.score = rep.score + points;
rep.verified_submissions = rep.verified_submissions + 1;
if (rep.verified_submissions >= 10) {
rep.trust_level = 2;
} else if (rep.verified_submissions >= 3) {
rep.trust_level = 1;
}
} else {
rep.rejected_count = rep.rejected_count + 1;
if (rep.score >= points / 2) {
rep.score = rep.score - (points / 2);
} else {
rep.score = 0;
}
}
}
}
Markets and Resolution Receipts
module pml::prediction_market {
use sui::event;
struct Market has key {
id: UID,
story_id: ID,
question: String,
oracle_config: NautilusConfig,
resolution: Option<ResolutionReceipt>,
deadline: u64
}
/// Nautilus enclave configuration for oracles
struct NautilusConfig has store {
pcr_0: vector<u8>, // Platform Configuration Register 0
pcr_1: vector<u8>, // Platform Configuration Register 1
pcr_2: vector<u8>, // Platform Configuration Register 2
public_key: vector<u8>, // Enclave's ephemeral public key
allowed_domains: vector<String>, // Whitelisted external domains
min_sources: u8, // Minimum confirming sources required
confidence_threshold: u64 // Minimum confidence score (0-10000 basis points)
}
/// Comprehensive attestation document from Nautilus
struct NautilusAttestation has store, copy, drop {
module_id: vector<u8>, // Hash of enclave image
digest: vector<u8>, // SHA-384 digest
timestamp: u64, // Unix timestamp from enclave
pcrs: vector<vector<u8>>, // All PCR values [PCR0, PCR1, PCR2]
certificate: vector<u8>, // AWS certificate chain
public_key: vector<u8>, // Ephemeral key for this session
user_data: vector<u8>, // Computation result data
signature: vector<u8> // Signature over computation hash
}
struct ResolutionReceipt has store, copy, drop {
market_id: ID,
outcome: bool,
confidence_score: u64, // 0-10000 basis points
sources_confirming: u8,
evidence_hashes: vector<vector<u8>>,
nautilus_attestation: NautilusAttestation,
computation_proof: vector<u8>,
timestamp: u64
}
/// Resolve market with Nautilus attestation verification
public fun resolve_with_nautilus_attestation(
market: &mut Market,
outcome: bool,
confidence_score: u64,
sources_confirming: u8,
evidence_hashes: vector<vector<u8>>,
attestation: NautilusAttestation,
computation_proof: vector<u8>,
clock: &Clock
) {
assert!(option::is_none(&market.resolution), E_ALREADY_RESOLVED);
// Verify Nautilus enclave attestation
verify_nautilus_attestation(&market.oracle_config, &attestation);
// Verify computation requirements met
assert!(
sources_confirming >= market.oracle_config.min_sources,
E_INSUFFICIENT_SOURCES
);
assert!(
confidence_score >= market.oracle_config.confidence_threshold,
E_LOW_CONFIDENCE
);
// Verify signature over computation
verify_enclave_signature(
&computation_proof,
&attestation.signature,
&attestation.public_key
);
let receipt = ResolutionReceipt {
market_id: object::id(market),
outcome,
confidence_score,
sources_confirming,
evidence_hashes,
nautilus_attestation: attestation,
computation_proof,
timestamp: clock::timestamp_ms(clock)
};
market.resolution = option::some(receipt);
event::emit(MarketResolvedByNautilus {
market_id: object::id(market),
outcome,
confidence_score,
enclave_digest: attestation.digest
});
}
/// Verify Nautilus enclave attestation against configuration
fun verify_nautilus_attestation(
config: &NautilusConfig,
attestation: &NautilusAttestation
) {
// Verify PCR measurements match registered enclave
assert!(
*vector::borrow(&attestation.pcrs, 0) == config.pcr_0 &&
*vector::borrow(&attestation.pcrs, 1) == config.pcr_1 &&
*vector::borrow(&attestation.pcrs, 2) == config.pcr_2,
E_PCR_MISMATCH
);
// Verify ephemeral public key matches registered key
assert!(
attestation.public_key == config.public_key,
E_KEY_MISMATCH
);
// Verify AWS certificate chain (simplified)
verify_aws_certificate_chain(&attestation.certificate);
// Additional attestation document validation
assert!(
verify_attestation_signature(&attestation.digest, &attestation.certificate),
E_INVALID_ATTESTATION
);
}
}
Events and client consumption
Each module emits structured events for: content minted/updated, entitlement minted/expired, bounty posted/resolved, reputation changed, market linked/resolved. Clients subscribe via RPC and maintain local indexes to offer rich UX without relying on a centralized API. See Sui’s transaction lifecycle for how certificates and effects surface to clients.
Upgrade and compatibility strategy
Packages use Sui’s upgrade policy with strict source compatibility. Data migrations are performed via upgrade entry functions that transform objects in place or via versioned parallel types when necessary. Events are versioned; clients gate behavior on observed package versions to preserve determinism.
Gas, fees, and economics
Users pay with SUI or a supported stablecoin. Gas usage for common flows (e.g., entitlement mint) is optimized by minimizing shared‑object interactions. Sponsorship policies cap exposure and require basic proof‑of‑person to deter abuse. Royalties and revenue splits are encoded in Kiosk policies for durable attribution.
Optional indexing (out of hot path)
While the reader request path has no traditional backend, teams may run an indexer for analytics and search. Sui's guides (for example, the trustless swap indexer and API pattern) illustrate how to project on‑chain objects into a local database with cursors and checkpoints. Such services are strictly auxiliary and never authoritative for access or settlement.
Platform Implementation Examples
Content Publishing with Kiosk v2
// Journalist publishes investigative article with royalty splits
async function publishInvestigation(
article: ArticleContent,
contributors: Address[],
royaltySplit: number[]
) {
// Upload article to Walrus via Quilt SDK
const quiltClient = new QuiltClient({ network: 'mainnet' });
const blobId = await quiltClient.upload({
data: article.content,
onProgress: (progress) => console.log(`Upload: ${progress}%`)
});
// Create Kiosk v2 TransferPolicy for complex royalties
const policy = await createTransferPolicy({
rules: [
{ type: 'royalty_rule',
percentage: 10, // 10% total royalty
distribution: royaltySplit,
recipients: contributors },
{ type: 'floor_price',
amount: article.price }
]
});
// Mint content NFT with metadata
const contentNFT = await mintContent({
kiosk_id: journalistKiosk,
metadata: {
title: article.title,
walrus_blob_id: blobId,
content_hash: hash(article),
embargo_until: article.embargoDate
},
transfer_policy: policy
});
return contentNFT;
}
Secure Source Submission with Seal
// Source submits confidential documents to journalist bounty
async function submitWhistleblowerDocs(
bountyId: string,
documents: File[],
journalistAddress: Address
) {
// Initialize Seal client with threshold encryption
const sealClient = new SealClient({
keyServers: [
'ks1.protocolmedia.io',
'ks2.protocolmedia.io',
'ks3.protocolmedia.io'
],
threshold: 2 // 2 of 3 servers needed for decryption
});
// Encrypt documents with access policy
const encrypted = await sealClient.encrypt({
data: documents,
policy: {
approvers: [journalistAddress, editorAddress],
conditions: {
reputation_threshold: 1000,
time_lock: Date.now() + 48 * 60 * 60 * 1000, // 48 hours
require_2fa: true
}
}
});
// Store encrypted blob on Walrus
const walrusId = await walrus.store(encrypted);
// Anchor submission on-chain
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::bounty::submit_evidence`,
arguments: [
tx.object(bountyId),
tx.pure(hash(encrypted)),
tx.pure(walrusId),
tx.pure(Date.now())
]
});
await signAndExecute(tx);
}
Reader Access with zkLogin and Sponsored Transactions
// New reader accesses premium content without holding SUI
async function accessPremiumContent(
contentId: ObjectId,
provider: 'google' | 'apple'
) {
// Generate ephemeral keys for zkLogin session
const ephemeralKeys = Ed25519Keypair.generate();
const maxEpoch = await getMaxEpoch();
// Authenticate with OAuth provider
const zkProof = await authenticateWithProvider({
provider,
ephemeralPublicKey: ephemeralKeys.getPublicKey(),
maxEpoch,
nonce: generateRandomness()
});
// Derive Sui address from zkLogin proof
const userAddress = deriveZkLoginAddress(zkProof);
// Check existing entitlements
const entitlements = await checkEntitlements(userAddress, contentId);
if (!entitlements.hasAccess) {
// Purchase access with sponsored transaction
const tx = new TransactionBlock();
tx.moveCall({
target: `${PACKAGE_ID}::entitlements::purchase_access`,
arguments: [
tx.object(contentId),
tx.payment(ARTICLE_PRICE)
]
});
// Get sponsor signature for gas
const sponsorSig = await requestSponsor({
transaction: tx,
userAddress,
action: 'content_purchase'
});
// Execute with both signatures
await executeSponsored(tx, zkProof, sponsorSig);
}
// Fetch and decrypt content
return await fetchContent(contentId, userAddress);
}
Prediction Market with Nautilus Oracle
// Create prediction market for story outcome
async function createPredictionMarket(
storyId: ObjectId,
question: string,
resolutionDate: Date
) {
// Deploy market with Nautilus oracle configuration
const tx = new TransactionBlock();
const [market] = tx.moveCall({
target: `${PACKAGE_ID}::prediction_market::create`,
arguments: [
tx.object(storyId),
tx.pure(question),
tx.pure({
oracle_type: 'nautilus',
enclave: {
measurement: NAUTILUS_ENCLAVE_HASH,
attestation_url: 'https://oracle.protocolmedia.io/attest'
},
resolution: {
type: 'web_evidence',
sources: ['reuters.com', 'apnews.com'],
threshold: 2 // Need 2 sources to confirm
}
}),
tx.pure(resolutionDate.getTime())
]
});
// Link market to story
tx.moveCall({
target: `${PACKAGE_ID}::content::link_market`,
arguments: [tx.object(storyId), market]
});
const result = await signAndExecute(tx);
// Emit market creation event
return extractMarketId(result);
}