Authenticated Events
Authenticated events provide a cryptographically verifiable stream of Move events from Sui smart contracts. Unlike regular events, authenticated events support verification by a light client without trusting any intermediary, which makes them suitable for applications that require trustless event consumption.
Why authenticated events?
Full nodes index regular Sui events and you can query them, but verifying them requires trusting the RPC provider. Authenticated events solve this by:
- Cryptographic commitments: Sui commits events into a Merkle mountain range (MMR) structure stored onchain (see the MMR section).
- Checkpoint verification: Sui ties the commitment to its checkpoints, which the validator committee signs.
- Light client verification: Clients verify the chain from the genesis committee, to checkpoint signatures, to event inclusion proofs.
This enables cryptographic verification of event completeness and correctness, with no full node required, and is lightweight enough to run in a browser or on mobile.
Quick start
Use regular events for indexing, telemetry, and dashboards where trusting an RPC provider or indexer is acceptable. Use authenticated events when a consumer needs cryptographic proof of event completeness and correctness rather than trusting an intermediary, such as a light client, bridge, or cross-chain workflow. See Security Best Practices for guidance on choosing trust boundaries.
Use the following examples to emit authenticated events from a Move contract and consume them from a client.
How do I emit authenticated events from Move?
The following Move module emits an authenticated event:
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(),
});
}
Sui automatically associates the event with the package that defines the event type. Only that package can emit events to its stream. The stream_id of that stream is the original ID of the package that defines the event type (the address of its first published version), which the example in How do I consume and verify events in Rust? builds with SuiAddress::from(package_id).
Are authenticated events backward compatible with regular events?
Authenticated events are backward compatible with existing event consumers. An authenticated event is a regular event with additional metadata for verification, so existing event consumers continue to work unchanged. Emitting authenticated events, however, requires a contract change and a package upgrade.
To upgrade:
- Contract change: Replace
event::emit(...)calls withevent::emit_authenticated(...)in your Move code. - Package upgrade: Deploy the updated package (requires a contract upgrade).
- Optional: Update consumers to use the authenticated events client for cryptographic verification.
Existing indexers, explorers, and tools that consume regular events continue to see authenticated events without modification.
How do I consume and verify events in Rust?
The reference Rust client verifies and streams events from a package. The example assumes your application defines rpc_url, genesis_committee, and package_id, and that the code runs in an async function returning a Result so that ? can propagate errors:
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;
}
}
}
How do I resume a stream from a checkpoint?
To resume a stream, the client needs a verified starting state. To guarantee completeness, this requires an Object Checkpoint State (OCS) inclusion proof showing the EventStreamHead at that checkpoint, which only exists if the package emitted authenticated events for the stream at that checkpoint. In practice, this is 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 fails to initialize.
This snippet reuses the client and stream_id created in the previous example.
let last_checkpoint = 12345;
let mut stream = client.clone()
.stream_events_from_checkpoint(stream_id, last_checkpoint)
.await?;
Architecture
The following diagram shows the flow from event emission to light client verification:
┌─────────────────────────────────────────────────────────────────┐
│ Move Contract │
│ event::emit_authenticated(MyEvent { ... }) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────── ───────────┐
│ EventStreamHead (Onchain) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 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 LedgerService.ListEvents RPC │
│ 4. Recompute MMR from events, compare to EventStreamHead │
└─────────────────────────────────────────────────────────────────┘
Key components
| Component | Description |
|---|---|
emit_authenticated<T>() | Move function to emit verified events |
EventStreamHead | Onchain commitment structure per package |
LedgerService.ListEvents | gRPC API that streams the events of a stream |
AuthenticatedEventsClient | Reference Rust client for verification |
| MMR (Merkle mountain range) | Append-only commitment structure |
emit_authenticated and accumulator settlement
You call emit_authenticated() to emit authenticated events. This writes a regular event alongside metadata including the stream_id to transaction effects.
The accumulator settlement process batches and settles events at checkpoint boundaries:
- Event emission: During transaction execution, the transaction emits authenticated events.
- Consensus commit batching: Sui groups authenticated events by consensus commit and computes a merkle tree over events for each
stream_id. - Checkpoint settlement: At each checkpoint, validators execute a settlement transaction that:
- Appends each consensus commit's merkle root to the stream's MMR
- Updates the
EventStreamHeadobject for thatstream_idwith the new MMR state and event count
Each consensus commit's merkle root becomes one leaf of the stream's MMR, so the per-commit merkle trees sit beneath the MMR rather than replacing it. See Merkle Mountain Range (MMR) for how the MMR organizes those leaves.
API reference
Authenticated events use two services: the standard ledger service to read events, and the proof service to verify that Sui wrote the onchain commitment at a given checkpoint.
LedgerService.ListEvents
Clients read events through the standard LedgerService.ListEvents server-streaming RPC in sui.rpc.v2. Authenticated events are ordinary events carrying extra metadata, so they need no separate event API.
service LedgerService {
rpc ListEvents(ListEventsRequest) returns (stream ListEventsResponse);
}
The reference client sets:
read_mask: the fields it needs, which areevent_type,contents,checkpoint,transaction_index, andevent_indexstart_checkpoint: the checkpoint to resume fromfilter: restricts the stream to the events of onestream_idoptions: query options, including the page size
Because the RPC streams, the client reads response frames until the server signals an end reason, and each frame carries a watermark.cursor it records for resumption.
ProofService.GetCheckpointObjectProof
The proof service proves what happened to an object at a specific checkpoint. It lives in sui.rpc.v2alpha.
service ProofService {
rpc GetCheckpointObjectProof(GetCheckpointObjectProofRequest)
returns (GetCheckpointObjectProofResponse);
}
Use this RPC to verify that Sui wrote the EventStreamHead object at a specific checkpoint.
Request parameters:
object_id(required): TheEventStreamHeadobject ID, hex-encoded, derived from the package addresscheckpoint(required): Checkpoint sequence number to query
Response:
checkpoint_summary: BCS-encodedCertifiedCheckpointSummary, carrying thecheckpoint_artifacts_digestthat the proof'stree_rootmust reconstruct, and the BLS aggregate signature over itproof: aoneofthat is eitherinclusion(OcsInclusionProof): the object changed in this checkpoint. Containsobject_ref,merkle_proof,leaf_index,tree_root, andobject_data, the BCS-encoded object at the committed version.object_datais absent when the change was a deletion or a wrap.non_inclusion(OcsNonInclusionProof): the object did not change in this checkpoint
The inclusion proof verifies that Sui wrote the EventStreamHead at the specified checkpoint. This is essential for resuming streams: the checkpoint must be one where the package actually emitted events.
Client configuration
Configure the client with custom pagination, polling, and timeout settings:
let config = ClientConfig::new(
page_size, // Events per RPC call
poll_interval, // How often to poll for new events
rpc_timeout, // RPC call timeout
)?;
let client = AuthenticatedEventsClient::new_with_config(
rpc_url,
genesis_committee,
config
).await?;
Merkle mountain range (MMR)
An MMR is a forest of perfect binary trees (mountains). After the MMR receives n leaves, mountain sizes correspond to the set bits in the binary representation of n. For example, when n = 13, which is 8 + 4 + 1 (1101 in binary, with bits 3, 2, and 0 set), the mountains have sizes 2^3 = 8, 2^2 = 4, and 2^0 = 1. The diagram in Expanded MMR with 13 leaves shows an expanded MMR (internal nodes and leaves).
EventStreamHead stores the MMR state as a compact vector of digests indexed by tree height (not the full expanded tree). This keeps onchain state O(log n) in the number of appended leaves.
Key properties of an MMR:
- Compact onchain accumulator state:
O(log n)digests. - Append-only updates: you can incorporate new leaves using only the current accumulator state (without the expanded MMR).
- Inclusion proofs are
O(log n)in size. Current RPC services do not yet expose event-level MMR inclusion proofs (this is separate from the OCS inclusion proofs thatProofServiceexposes, which prove that Sui wrote theEventStreamHeadobject at a checkpoint, not that an individual event is in the MMR). These inclusion proofs can lead to many interesting applications, such as moving data storage offchain, efficient cross-chain reads, and proving inclusion in zero-knowledge (ZK) proofs.
Expanded MMR with 13 leaves
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
Leaves (Lx) correspond to the per-consensus-commit merkle tree digests, which in turn contain AuthenticatedEvent as their leaves.
Trust model
The client establishes trust through the following chain:
- Genesis committee: You initialize the client with the genesis validator committee public keys.
- Trust ratcheting: When epochs change, the client verifies that the previous committee signed the new one.
- Checkpoint verification: The client verifies each checkpoint summary against the committee's aggregate signature.
- Object inclusion proof: An inclusion proof shows that a specific checkpoint modified the
EventStreamHeadobject. - MMR verification: The client verifies events against the
EventStreamHead's MMR commitment.
The client verifies a checkpoint range by:
- Starting with a verified
EventStreamHeadfrom the preceding checkpoint (or empty state for a new stream) - Fetching events in the range and appending them to the MMR
- Fetching the
EventStreamHeadat the final checkpoint and proving its inclusion through an OCS proof - Comparing the locally computed MMR against the onchain
EventStreamHead
If any event is missing, modified, or out of order, the computed MMR does not match the onchain state.
The client automatically handles epoch transitions and committee changes.
Security guarantees
Completeness: When you receive verified event N from a stream, verification guarantees that you 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 (for example, an RPC provider) has no obligation to return all events up to a requested checkpoint height. To detect withholding, clients must verify against the onchain EventStreamHead, which contains the authoritative event count. The reference client does this automatically. If the events received do not match the onchain commitment, verification fails.
Correctness: Sui commits each event's content to the MMR. Any modification to event data would cause the computed MMR to diverge from the onchain state.
Limitations
- You can retrieve events only if the full node indexes them and has not pruned them.
- You must consume events in checkpoint order.
- The
EventStreamHeadmust change at the resume checkpoint (the package must have emitted authenticated events). - Each package has one stream (
stream_id= package address), so all authenticated event types that the package defines share that stream. - Sui does not support event-level MMR inclusion proofs.
Error handling
The following table summarizes the error types and how the client handles them:
| Error type | Recoverable | Action |
|---|---|---|
TransportError | Yes | Automatic retry |
RpcError (Unavailable, DeadlineExceeded) | Yes | Automatic retry |
VerificationError | No | Stop. Data integrity issue; try a different RPC endpoint |
InternalError | No | Stop. Invalid state; investigate the cause |
The stream automatically retries transient errors. Terminal errors indicate verification failures or invalid state and require investigation.