Event Streaming
Tenzro provides a unified event streaming system that delivers real-time notifications for every on-chain action across all three VMs (EVM, SVM, DAML). Whether you are building a wallet, an indexer, an AI agent, or a compliance dashboard, the same event bus feeds every consumer through four delivery channels: WebSocket, gRPC streaming, webhooks, and MCP tools. Events are durably stored so consumers can replay history from any point using cursor-based pagination.
Architecture Overview
The event pipeline is structured as a series of layers. At the core sits a lock-free ring buffer inspired by Monad's asynchronous execution design, which decouples event production from consumption so that consensus and execution are never blocked by slow subscribers. Events flow outward through four fan-out channels, each optimized for a different consumer profile. An append-only EventStore (backed by RocksDB column family CF_EVENTS) retains events for historical replay, taking cues from Reth's ExEx framework for reorg-aware delivery and Sui's cursor-based checkpoint model for deterministic replay.
The ring buffer uses a monotonically increasing sequence number (u64) as its cursor. Every event is assigned a sequence at write time, giving consumers a total ordering that survives reconnections. When a chain reorganization occurs, the EventBus emits a ChainReorg event containing the fork point sequence, allowing subscribers to roll back and re-process affected blocks — similar to the approach taken by Solana's Yellowstone gRPC plugin for in-process event delivery.
Event Types
All events share a common envelope containing a sequence number, timestamp, block height, and transaction hash (where applicable). The inner payload is the TenzroEvent enum, which covers every subsystem in the network.
| Category | Event | Key Fields |
|---|---|---|
| Block | BlockProposed | height, proposer, tx_count, parent_hash |
BlockFinalized | height, state_root, gas_used, elapsed_ms | |
ChainReorg | fork_height, old_head, new_head, depth | |
| Transaction | TxSubmitted | tx_hash, from, to, value, nonce, vm_type |
TxConfirmed | tx_hash, block_height, gas_used, status | |
TxFailed | tx_hash, reason, revert_data | |
| EVM Logs | EvmLog | address, topics[], data, log_index |
| Token / NFT | TokenTransfer | token_id, from, to, amount, vm_type |
TokenCreated | token_id, symbol, decimals, creator | |
NftMinted | collection, token_id, owner, metadata_uri | |
| Identity | IdentityRegistered | did, identity_type, controller_did |
IdentityRevoked | did, reason, cascade_count | |
CredentialIssued | credential_type, issuer_did, subject_did, expires_at | |
| AI / Agent | InferenceCompleted | model_id, provider, tokens_used, latency_ms, cost |
AgentSpawned | agent_id, template_id, controller_did | |
AgentMessage | from_agent, to_agent, message_type, payload_hash | |
| Settlement | SettlementCompleted | settlement_id, payer, payee, amount, protocol |
ChannelOpened | channel_id, parties, deposit, ttl | |
| Staking | StakeDeposited | validator, amount, total_stake, role |
ValidatorSlashed | validator, slash_amount, reason, evidence_hash | |
ProposalCreated | proposal_id, proposer, title, voting_end | |
| Bridge | BridgeInitiated | bridge_id, src_chain, dst_chain, amount, adapter |
BridgeCompleted | bridge_id, dst_tx_hash, elapsed_secs |
Delivery Channels
Tenzro exposes the same events through four delivery channels, each suited to different latency requirements and integration patterns.
| Channel | Protocol | Typical Consumer | Latency | Endpoint |
|---|---|---|---|---|
| WebSocket | JSON-RPC 2.0 over WS | Wallets, dApps, explorers | ~5 ms | ws://localhost:8545 |
| gRPC Streaming | Bidirectional Protobuf | Indexers, validators, analytics | ~3 ms | localhost:3008 |
| Webhooks | HTTP POST + HMAC-SHA256 | Bots, backends, alerting | 100 ms - 5 s | Configured via RPC |
| MCP Tools | JSON-RPC over HTTP | AI agents, LLM integrations | ~10 ms | localhost:3001/mcp |
All four channels share the same event sequence numbering, so a consumer can switch delivery methods without missing events by providing the last-seen sequence as a cursor.
WebSocket Subscriptions
The WebSocket interface is fully compatible with the Ethereum eth_subscribe specification, so existing tools like viem, ethers.js, and web3.js work without modification. Tenzro extends the standard subscription types with a tenzroEvents type for cross-VM event streaming.
Subscribe to New Blocks
// Request — subscribe to new block headers
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribe",
"params": ["newHeads"]
}
// Response — subscription ID
{
"jsonrpc": "2.0",
"id": 1,
"result": "0xa1b2c3d4e5f60001"
}
// Notification — pushed on each new block
{
"jsonrpc": "2.0",
"method": "eth_subscription",
"params": {
"subscription": "0xa1b2c3d4e5f60001",
"result": {
"number": "0x1f4",
"hash": "0xabc...def",
"parentHash": "0x123...789",
"stateRoot": "0xfed...321",
"timestamp": "0x66a2b3c4",
"gasUsed": "0xe4e1c0",
"transactionCount": 42
}
}
}Subscribe to EVM Logs
// Subscribe to Transfer events on the wTNZO ERC-20 contract
{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_subscribe",
"params": [
"logs",
{
"address": "0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}
]
}Subscribe to Tenzro Events
// Subscribe to settlement and inference events across all VMs
{
"jsonrpc": "2.0",
"id": 3,
"method": "eth_subscribe",
"params": [
"tenzroEvents",
{
"event_types": ["SettlementCompleted", "InferenceCompleted"],
"from_sequence": 50000
}
]
}
// Notification — settlement event
{
"jsonrpc": "2.0",
"method": "eth_subscription",
"params": {
"subscription": "0xa1b2c3d4e5f60003",
"result": {
"sequence": 50142,
"event_type": "SettlementCompleted",
"block_height": 512,
"timestamp": 1720300800,
"data": {
"settlement_id": "settle_abc123",
"payer": "0x1234...abcd",
"payee": "0x5678...ef01",
"amount": "1500000000000000000",
"protocol": "mpp"
}
}
}
}Supported subscription types: newHeads, logs, newPendingTransactions, syncing, and tenzroEvents. Use eth_unsubscribe to cancel a subscription.
gRPC Streaming
For high-throughput consumers like indexers and analytics pipelines, the gRPC streaming interface on port 3008 provides the lowest latency (~3 ms) with Protobuf-encoded events. The service supports bidirectional streaming, allowing clients to dynamically update their filter without dropping the connection.
// Proto definition (proto/tenzro/v1/events.proto)
service EventStream {
// Server-streaming: client sends a filter, server pushes matching events
rpc Subscribe (EventFilter) returns (stream EventEnvelope);
// Bidirectional: client can update filters mid-stream
rpc SubscribeDynamic (stream EventFilter) returns (stream EventEnvelope);
// Unary: fetch a page of historical events
rpc GetEvents (GetEventsRequest) returns (GetEventsResponse);
}
message EventFilter {
repeated string event_types = 1; // e.g. ["BlockFinalized", "TokenTransfer"]
repeated string addresses = 2; // filter by involved address
repeated bytes topics = 3; // EVM log topic0 filters
repeated string vm_types = 4; // "evm", "svm", "daml"
uint64 from_sequence = 5; // cursor for replay
}
message EventEnvelope {
uint64 sequence = 1;
uint64 timestamp = 2;
uint64 block_height = 3;
bytes tx_hash = 4;
TenzroEvent event = 5;
}Rust Client Example
use tenzro_sdk::events::EventStreamClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut client = EventStreamClient::connect("http://localhost:3008").await?;
let filter = EventFilter {
event_types: vec!["TokenTransfer".into(), "SettlementCompleted".into()],
from_sequence: 0, // start from the beginning
..Default::default()
};
let mut stream = client.subscribe(filter).await?.into_inner();
while let Some(envelope) = stream.message().await? {
println!(
"seq={} height={} event={:?}",
envelope.sequence, envelope.block_height, envelope.event
);
}
Ok(())
}The from_sequence field enables cursor-based replay. When a consumer reconnects after a disconnect, it passes its last processed sequence number and receives all subsequent events without gaps. The EventStore retains events for a configurable retention period (default: 7 days, or 10 million events, whichever comes first).
Webhooks
Webhooks deliver events as HTTP POST requests to a URL you control. Every webhook payload is signed with HMAC-SHA256 so your backend can verify authenticity. Tenzro supports dual delivery: unconfirmed events are sent immediately for low-latency alerting, and confirmed events are sent after finalization for reliable processing.
Register a Webhook
// Register via JSON-RPC
{
"jsonrpc": "2.0",
"id": 1,
"method": "tenzro_registerWebhook",
"params": [{
"url": "https://api.example.com/tenzro/events",
"secret": "whsec_a1b2c3d4e5f6...",
"filter": {
"event_types": ["TxConfirmed", "ValidatorSlashed"],
"addresses": ["0x1234...abcd"]
},
"delivery": "confirmed",
"max_retries": 5
}]
}
// Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"webhook_id": "wh_7f8g9h0i",
"status": "active",
"created_at": 1720300800
}
}Webhook Payload Format
// HTTP POST to your endpoint
// Headers:
// Content-Type: application/json
// X-Tenzro-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015...
// X-Tenzro-Webhook-Id: wh_7f8g9h0i
// X-Tenzro-Timestamp: 1720300800
// X-Tenzro-Sequence: 50142
{
"webhook_id": "wh_7f8g9h0i",
"sequence": 50142,
"timestamp": 1720300800,
"delivery": "confirmed",
"event": {
"type": "TxConfirmed",
"block_height": 512,
"tx_hash": "0xabc...def",
"data": {
"from": "0x1234...abcd",
"to": "0x5678...ef01",
"value": "1000000000000000000",
"gas_used": 21000,
"status": "success"
}
}
}To verify the signature, compute HMAC-SHA256(secret, timestamp + "." + body) and compare against the X-Tenzro-Signature header value. Deliveries that receive a non-2xx response are retried with exponential backoff (1s, 2s, 4s, 8s, 16s) up to the configured max_retries. After all retries are exhausted, the webhook is marked as degraded and an alert is emitted.
Event Filtering
All delivery channels accept the same EventFilter structure. The filter is compatible with Ethereum's eth_getLogs address and topic filtering, extended with Tenzro-specific fields for cross-VM event selection.
// EventFilter structure
{
// Ethereum-compatible fields
"address": "0x7a4b...7f93" | ["0x7a4b...7f93", "0xdead...beef"],
"topics": [
"0xddf252ad...", // topic0: event signature
null, // topic1: any value (wildcard)
"0x00000000...1234abcd" // topic2: specific indexed param
],
"fromBlock": "0x100", // start block (inclusive)
"toBlock": "latest", // end block (inclusive)
// Tenzro extensions
"event_types": ["TokenTransfer", "InferenceCompleted"],
"vm_types": ["evm", "svm"],
"from_sequence": 50000, // cursor-based replay start
"limit": 1000 // max events per response
}Filters are evaluated as a conjunction: all specified conditions must match for an event to be delivered. Within array fields, values are disjunctive (any match suffices). For example, specifying both event_types: ["TokenTransfer", "NftMinted"] and vm_types: ["evm"] returns EVM token transfers and EVM NFT mints, but not SVM token transfers.
Historical Queries
Historical events can be retrieved through the standard eth_getLogs RPC for EVM log queries, or through the Tenzro-specific tenzro_getEvents method for cross-VM event replay with cursor-based pagination.
// eth_getLogs — standard EVM log query
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [{
"fromBlock": "0x1",
"toBlock": "latest",
"address": "0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}]
}
// tenzro_getEvents — cross-VM historical query with cursor pagination
{
"jsonrpc": "2.0",
"id": 2,
"method": "tenzro_getEvents",
"params": [{
"event_types": ["SettlementCompleted", "BridgeCompleted"],
"from_sequence": 0,
"limit": 100
}]
}
// Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"events": [
{
"sequence": 1042,
"event_type": "SettlementCompleted",
"block_height": 100,
"timestamp": 1720200000,
"data": { ... }
}
],
"next_cursor": 1142,
"has_more": true
}
}To page through results, pass the next_cursor value as from_sequence in the next request. Continue until has_more is false.
CLI Usage
The tenzro events command group provides a convenient interface for subscribing to live events, querying history, and managing webhooks from the terminal.
# Subscribe to all events in real-time (WebSocket)
tenzro events subscribe
# Subscribe with filters
tenzro events subscribe --types TokenTransfer,InferenceCompleted --vm evm
# Resume from a known sequence (cursor-based recovery)
tenzro events subscribe --from-sequence 50000
# Query historical events
tenzro events history --types SettlementCompleted --limit 50
# Query EVM logs for a specific contract
tenzro events logs \
--address 0x7a4bcb13a6b2b384c284b5caa6e5ef3126527f93 \
--topic0 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef \
--from-block 1 --to-block latest
# Register a webhook
tenzro events register-webhook \
--url https://api.example.com/events \
--secret whsec_mySecret123 \
--types TxConfirmed,ValidatorSlashed \
--delivery confirmed
# List active webhooks
tenzro events info
# Output as JSON for piping to jq
tenzro events subscribe --types BlockFinalized --format json | jq '.block_height'Best Practices
Polling vs. WebSocket
While WebSocket subscriptions offer the lowest latency, libraries like viem default to polling for reliability. For most dApp use cases, polling eth_getFilterChanges every 4 seconds is sufficient and avoids the complexity of managing reconnection logic. Reserve WebSocket subscriptions for latency-sensitive applications like trading bots or real-time dashboards.
Cursor-Based Recovery
Always persist the last-processed sequence number in your consumer. On reconnect, pass it as from_sequence to resume exactly where you left off. This guarantees at-least-once delivery without requiring the server to track consumer state. Consumers should be idempotent — use the sequence number or tx_hash as a deduplication key.
HMAC Verification
Always verify the X-Tenzro-Signature header on webhook payloads using a constant-time comparison. Check the X-Tenzro-Timestamp to reject payloads older than 5 minutes, preventing replay attacks. Never log the webhook secret or include it in error responses.
Rate Limiting
The WebSocket and gRPC interfaces enforce per-connection rate limits. WebSocket subscriptions are capped at 10,000 events per second per connection. If your consumer falls behind, events are buffered up to the ring buffer capacity (65,536 events), after which the oldest undelivered events are dropped and a SubscriptionOverflow notification is sent with the gap range. Use from_sequence to recover the missed events from the EventStore.
Handling Chain Reorganizations
When a ChainReorg event arrives, roll back any local state derived from blocks above the fork_height and re-process events from that point. For webhooks, the confirmed delivery mode avoids this entirely by only sending events after finalization. For WebSocket and gRPC consumers that need instant updates, subscribe to ChainReorg alongside your primary events and implement rollback logic.