ENVIO HyperIndex with Oyster CVM
HyperIndex is Envio’s full-featured blockchain indexing framework that converts on-chain events into structured, queryable databases with GraphQL APIs. While HyperIndex offers fast access to on-chain data, the default setup does not provide verification or confidentiality.
Oyster CVM solves this by running the indexer inside a Trusted Execution Environment, providing two key security guarantees:
1. Confidentiality
The HyperIndex runs inside Oyster CVM’s Trusted Execution Environment (TEE), an isolated runtime environment designed to protect data and execution from external access. This hardware-enforced isolation ensures confidentiality, since no external process, including the host OS or cloud provider, can modify the code or data running inside the enclave.
- Data Protection: All data processed by the indexer, including on-chain events, user balances and database entries, remains protected from unauthorized access by the host operating system, cloud provider administrators or external attackers.
- Code Isolation: The indexer’s execution environment keeps its logic fully isolated, preventing anyone outside the enclave from modifying the code while it is running.
2. Verifiability through Attestation
Attestation enables a remote user to verify that the Oyster CVM enclave is running the expected, untampered version of the HyperIndex indexer. This verification is rooted in the Nitro Security Module, which acts as the hardware-backed trust anchor for the Nitro system.
During attestation, the enclave produces an Attestation Document that the Nitro Security Module signs. This document contains cryptographic measurements, including PCR values (Platform Configuration Registers). These values are hashes representing the enclave’s code, configuration, and image identity.
By comparing these measurements with the expected values, a user can confirm that:
- The enclave is running the intended HyperIndex build.
- The code and configuration remain unchanged.
- The environment is a valid, attested Oyster CVM TEE instance.
This provides strong integrity guarantees and ensures that the results produced by HyperIndex are verifiable and come from a securely attested environment.
How we ran HyperIndex in Oyster CVM
For our setup, we chose to index an ERC20 token contract deployed on the Ethereum mainnet and index its transfers in order to fetch user balances by their wallet addresses.
-
The setup began by initializing an Envio project using the command:
pnpx envio initThe Envio command line utility prompts for details such as the blockchain ecosystem, contract address, and the events to index, then generates a starter template for the indexer project.
-
The next step was to copy the contract ABI into the project directory and update the config.yaml file to include the abi_file_path. The final config.yaml looked like this:
name: envio-indexer networks: - id: 1 start_block: 23370920 contracts: - name: YBToken abi_file_path: ./abis/token.json address: - 0x01791F726B4103694969820be083196cC7c045fF handler: src/EventHandlers.ts events: - event: Approval(address indexed owner, address indexed spender, uint256 value) - event: OwnershipTransferred(address indexed previous_owner, address indexed new_owner) - event: RoleMinterChanged(address indexed minter, bool status) - event: Transfer(address indexed sender, address indexed receiver, uint256 value) unordered_multichain_mode: true preload_handlers: true -
Next, to index user balances, we created a new User entity in schema.graphql file and modified the transfer event handler to fetch each user’s balance using their wallet address.
New entity in schema.graphql:
type User @entity { id: ID! address: String! balance: BigInt! }Handler:
YBToken.Transfer.handler(async ({ event, context }) => { const entity: YBToken_Transfer = { id: `${event.chainId}_${event.block.number}_${event.logIndex}`, sender: event.params.sender, receiver: event.params.receiver, value: event.params.value, }; context.YBToken_Transfer.set(entity); const from = event.params.sender; const to = event.params.receiver; const amount = event.params.value; const ZeroAddress = "0x0000000000000000000000000000000000000000"; if (from !== ZeroAddress) { const sender = await context.User.get(from); if (sender) { context.User.set({ ...sender, balance: sender.balance - amount, }); } } const receiver = await context.User.get(to); if (receiver) { context.User.set({ ...receiver, balance: receiver.balance + amount, }); } else { context.User.set({ id: to, address: to, balance: amount, }); } }); -
To run the indexer in Oyster CVM, we used the Dockerfile and docker-compose configuration file provided by the Envio team for the indexer, its database, and the GraphQL engine.
For the docker-compose setup, we switched to host network mode and updated the local IP references accordingly. The modified docker-compose file we used is shown below.
services: envio-postgres: image: postgres:17.5 restart: always ports: - "${ENVIO_PG_PORT:-5433}:5432" volumes: - envio-indexer-storage:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${ENVIO_PG_PASSWORD:-testing} POSTGRES_USER: ${ENVIO_PG_USER:-postgres} POSTGRES_DB: ${ENVIO_PG_DATABASE:-envio-dev} network_mode: host graphql-engine: image: hasura/graphql-engine:v2.43.0 ports: - "${HASURA_EXTERNAL_PORT:-8080}:8080" user: 1001:1001 depends_on: - "envio-postgres" restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://${ENVIO_PG_USER:-postgres}:${ENVIO_PG_PASSWORD:-testing}@localhost:5432/${ENVIO_PG_DATABASE:-envio-dev} HASURA_GRAPHQL_ENABLE_CONSOLE: ${HASURA_GRAPHQL_ENABLE_CONSOLE:-true} # can make this default to false once we are further in our development. HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log HASURA_GRAPHQL_NO_OF_RETRIES: 10 HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET:-testing} HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES: "true" PORT: 8080 HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public healthcheck: test: timeout 1s bash -c ':> /dev/tcp/0.0.0.0/8080' || exit 1 interval: 5s timeout: 2s retries: 50 start_period: 5s network_mode: host envio-indexer: image: sagarparker/enviotest:latest ports: - "0.0.0.0:9898:${ENVIO_INDEXER_PORT:-9898}" depends_on: graphql-engine: condition: service_healthy deploy: resources: limits: cpus: "0.8" memory: 800M restart: always environment: ENVIO_PG_PASSWORD: ${ENVIO_PG_PASSWORD:-testing} ENVIO_PG_HOST: localhost ENVIO_PG_PORT: 5432 ENVIO_PG_USER: ${ENVIO_PG_USER:-postgres} ENVIO_PG_DATABASE: ${ENVIO_PG_DATABASE:-envio-dev} ENVIO_PG_PUBLIC_SCHEMA: ${ENVIO_PG_PUBLIC_SCHEMA:-envio} ENVIO_INDEXER_HOST: 0.0.0.0 ENVIO_INDEXER_PORT: ${ENVIO_INDEXER_PORT:-9898} CONFIG_FILE: ${CONFIG_FILE:-config.yaml} LOG_LEVEL: ${LOG_LEVEL:-trace} LOG_STRATEGY: ${LOG_STRATEGY:-console-pretty} HASURA_GRAPHQL_ENDPOINT: http://localhost:8080/v1/metadata HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET:-testing} HASURA_SERVICE_HOST: localhost HASURA_SERVICE_PORT: 8080 TUI_OFF: ${TUI_OFF:-true} healthcheck: test: timeout 1s bash -c ':> /dev/tcp/0.0.0.0/8080' || exit 1 interval: 5s timeout: 2s retries: 50 start_period: 5s network_mode: host volumes: envio-indexer-storage: -
The final step for the indexer deployment was to install the oyster-cvm CLI and deploy the enclave.
Command for installing oyster-cvm CLI:
# for linux with amd64 architecture sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_linux_amd64 -O /usr/local/bin/oyster-cvm # for linux with arm64 architecture sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_linux_arm64 -O /usr/local/bin/oyster-cvm # for darwin with arm64 architecture (M series Macs) sudo wget https://artifacts.marlin.org/oyster/binaries/oyster-cvm_latest_darwin_arm64 -O /usr/local/bin/oyster-cvmCommand for deploying the oyster-cvm instance:
oyster-cvm deploy --wallet-private-key <YOUR_WALLET_PRIVATE_KEY> --duration-in-minutes 120 --docker-compose docker-compose.yaml --arch amd64 --instance-type c5.2xlargeOn successful deployment, the above command returns the instance ip which can be used to fetch user’s balance using a graphQL query.
-
Fetching user balances using a GraphQL query
The GraphQL query below returns a user’s on-chain token balance from the indexer running in Oyster CVM.
curl --location 'http://<OYSTER_CVM_INSTANCE_IP>:8080/v1/graphql' \ --header 'Content-Type: application/json' \ --data '{"query":"query MyQuery {\n User(where: {address: {_eq: \"0x4E9193C8CDe3A4f3dBE52ff61F66D9389ffBcaF0\"}}) {\n address\n balance\n }\n}","variables":{}}'The Hasura dashboard can be accessed by visiting:
http://<OYSTER_CVM_INSTANCE_IP>:8080/console -
Attestation verification with Oyster CVM CLI
The enclave attestation for the instance running the Envio indexer can be performed using the following command:
oyster-cvm verify --enclave-ip <OYSTER_CVM_INSTANCE_IP>Sample output :
Conclusion
Running HyperIndex inside Oyster CVM brings verifiability and confidentiality to blockchain indexing without compromising on speed or developer experience. The TEE-backed environment ensures that both the indexer’s data and execution remain protected, while attestation allows users to verify the integrity of the system. This combination provides a trustworthy foundation for building secure, data-driven applications on top of Envio’s indexing framework.

