docs/content/guides/developer/accessing-data/authenticated-events.mdx
Authenticated Events provide a cryptographically verifiable stream of Move events from Sui smart contracts. Unlike regular events, authenticated events can be verified by a light client without trusting any intermediary, making them suitable for applications requiring trustless event consumption.
Regular Sui events are indexed and queryable, but verifying them requires trusting the RPC provider. Authenticated events solve this by:
This enables cryptographic verification of event completeness and correctness — no full node required, and lightweight enough to run in a browser or on mobile.
module my_package::my_module;
use sui::event;
public struct MyEvent has copy, drop {
value: u64,
data: vector<u8>,
}
public entry fun do_something(value: u64) {
// Emit an authenticated event
event::emit_authenticated(MyEvent {
value,
data: vector::empty(),
});
}
The event is automatically associated with the package that defines the event type. Only that package can emit events to its stream.
Authenticated events are fully backwards-compatible with regular Sui events. An authenticated event is simply a regular event with additional metadata for verification — existing event consumers will continue to work unchanged.
To upgrade:
event::emit(...) calls with event::emit_authenticated(...) in your Move codeExisting indexers, explorers, and tools that consume regular events will continue to see authenticated events without modification.
use sui_light_client::authenticated_events::AuthenticatedEventsClient;
use sui_types::base_types::SuiAddress;
use futures::StreamExt;
use std::sync::Arc;
// Initialize with genesis committee (establishes trust root)
let client = Arc::new(
AuthenticatedEventsClient::new(rpc_url, genesis_committee)
.await?
);
// Stream events from your package
let stream_id = SuiAddress::from(package_id);
let mut stream = client.clone().stream_events(stream_id).await?;
while let Some(result) = stream.next().await {
match result {
Ok(event) => {
// event.event contains the verified Move event data
// event.checkpoint indicates when it was committed
println!("Verified event at checkpoint {}", event.checkpoint);
}
Err(e) => {
// Transient errors (TransportError, RpcError) are retried automatically.
// Terminal errors require action:
// - VerificationError: Data integrity issue, try a different RPC endpoint
// - InternalError: Invalid state, investigate the cause
// See "Error Handling" section for details.
eprintln!("Terminal error: {:?}", e);
break;
}
}
}
To resume a stream, the client needs a verified starting state. To guarantee completeness, this requires an OCS inclusion proof showing the EventStreamHead at that checkpoint — which only exists if authenticated events were emitted for the stream at that checkpoint. In practice, this would be the checkpoint sequence of the last event the client received before a disconnection.
If the specified checkpoint does not have events for that stream_id, the client will fail to initialize.
let last_checkpoint = 12345;
let mut stream = client.clone()
.stream_events_from_checkpoint(stream_id, last_checkpoint)
.await?;
┌─────────────────────────────────────────────────────────────────┐
│ Move Contract │
│ event::emit_authenticated(MyEvent { ... }) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ EventStreamHead (On-chain) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ mmr: vector<u256> // Merkle Mountain Range root │ │
│ │ checkpoint_seq: u64 // Last update checkpoint │ │
│ │ num_events: u64 // Total events in stream │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Light Client Verification │
│ 1. Fetch EventStreamHead with OCS inclusion proof │
│ 2. Verify proof against checkpoint (signed by committee) │
│ 3. Fetch events from ListAuthenticatedEvents RPC │
│ 4. Recompute MMR from events, compare to EventStreamHead │
└─────────────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
emit_authenticated<T>() | Move function to emit verified events |
EventStreamHead | On-chain commitment structure per package |
ListAuthenticatedEvents | gRPC API to fetch events with pagination |
AuthenticatedEventsClient | Reference Rust client for verification |
| MMR (Merkle Mountain Range) | Append-only commitment structure |
emit_authenticated and Accumulator SettlementUsers call emit_authenticated() to emit authenticated events. This writes a regular Event alongside metadata including the stream_id to Transaction Effects.
Events are batched and settled at checkpoint boundaries through the accumulator settlement process:
stream_idEventStreamHead object for that stream_id with the new MMR state and event countservice EventService {
rpc ListAuthenticatedEvents(ListAuthenticatedEventsRequest)
returns (ListAuthenticatedEventsResponse);
}
Request Parameters:
stream_id (required): Package address that emitted the eventsstart_checkpoint: Checkpoint to start from (default: 0)page_size: Events per page (default: 1000, max: 1000)page_token: Pagination token from previous responseResponse:
events: List of AuthenticatedEvent with checkpoint, transaction index, event index, and payloadhighest_indexed_checkpoint: Latest indexed checkpointnext_page_token: Token for next page (empty if no more events)service ProofService {
rpc GetObjectInclusionProof(GetObjectInclusionProofRequest)
returns (GetObjectInclusionProofResponse);
}
Used to verify that the EventStreamHead object exists at a specific checkpoint.
Request Parameters:
object_id (required): The EventStreamHead object ID (derived from package address)checkpoint (required): Checkpoint sequence number to prove inclusion atResponse:
object_ref: Object reference (object_id, version, digest)inclusion_proof: OCS (Object Checkpoint State) merkle proof containing:
merkle_proof: BCS-encoded proof nodesleaf_index: Position in the merkle treetree_root: Root digest (32 bytes)object_data: BCS-encoded EventStreamHead objectcheckpoint_summary: BCS-encoded checkpoint summary for verificationThe inclusion proof verifies that the EventStreamHead was written at the specified checkpoint. This is essential for resuming streams - the checkpoint must be one where events were actually emitted.
let config = ClientConfig::new(
page_size, // Events per RPC call (max 1000)
poll_interval, // How often to poll for new events
max_pagination_iterations, // Max pages before forcing checkpoint boundary
rpc_timeout, // RPC call timeout
)?;
let client = AuthenticatedEventsClient::new_with_config(
rpc_url,
genesis_committee,
config
).await?;
An MMR is a forest of perfect binary trees ("mountains"). After n leaves are appended, mountain sizes correspond to the set bits in the binary representation of n. For example, when n = 13 (1101 in binary with bit 3, 2 and 0 set), the mountains have sizes 2^3 = 8, 2^2 = 4, and 2^0 = 1. The diagram below shows an expanded MMR (internal nodes and leaves).
The MMR state in EventStreamHead is stored as a compact vector of digests indexed by tree height (not the full expanded tree). This keeps on-chain state O(log n) in the number of appended leaves.
Key properties of an MMR:
O(log n) digests.O(log n) in size. Event-level MMR inclusion proofs are not yet exposed by current RPC services (this is separate from OCS inclusion proofs exposed by the ProofService). These inclusion proofs can lead to many interesting applications, e.g., move data storage off-chain, efficient cross-chain reads, prove inclusion in ZK, etc. Tree 0 (size 8)
H0
┌────────┴────────┐
H1 H2
┌─────┴─────┐ ┌─────┴─────┐
H3 H4 H5 H6
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
L0 L1 L2 L3 L4 L5 L6 L7
Tree 1 (size 4)
H7
┌─────┴─────┐
H8 H9
┌──┴──┐ ┌──┴──┐
L8 L9 L10 L11
Tree 2 (size 1)
L12
Note that leaves (Lx) correspond to the per-consensus-commit merkle tree digests, which in turn contain AuthenticatedEvent as its leaves.
The client verifies a checkpoint range by:
EventStreamHead from the preceding checkpoint (or empty state for a new stream)EventStreamHead at the final checkpoint and proving its inclusion via OCS proofEventStreamHeadIf any event is missing, modified, or out of order, the computed MMR will not match the on-chain state.
The client automatically handles epoch transitions and committee changes.
Completeness: When you receive event N from a stream, you are guaranteed to have received all events 0..N-1 in the correct order. The MMR structure ensures that any missing or reordered events would cause verification to fail.
What completeness does NOT guarantee: An intermediary (e.g., RPC provider) is not obligated to return all events up to a requested checkpoint height. To detect withholding, clients must verify against the on-chain EventStreamHead which contains the authoritative event count. The reference client does this automatically — if the events received don't match the on-chain commitment, verification fails.
Correctness: Each event's content is committed to the MMR. Any modification to event data would cause the computed MMR to diverge from the on-chain state.
| Error Type | Recoverable | Action |
|---|---|---|
TransportError | Yes | Automatic retry |
RpcError (Unavailable, DeadlineExceeded) | Yes | Automatic retry |
VerificationError | No | Stop - data integrity issue |
InternalError | No | Stop - invalid state |
The stream automatically retries transient errors. Terminal errors indicate verification failures or invalid state and require investigation.